mirror of
https://github.com/KingHWJ/WeReadBoard.git
synced 2026-11-03 18:24:58 +08:00
新增功能: - 书籍详情弹窗:点击书架书籍展示详细信息(书名、作者、出版社、ISBN、字数、评分、简介、阅读进度),数据实时从微信读书 API 获取并入库 - 笔记详情弹窗:点击有笔记的书展示划线和想法/点评,按章节分组展示,数据实时获取并入库 - 推荐页面:个性化推荐(为你推荐)+ 关键词搜索书籍,数据从微信读书 API 获取并入库 - 修复 Flask 中文 URL 参数编码问题 后端新增 API: - GET /api/book/detail?book_id=xxx — 书籍详情 - GET /api/book/notes?book_id=xxx — 笔记详情(划线+想法) - GET /api/recommend?maxIdx=0 — 个性化推荐 - GET /api/search?keyword=xxx — 搜索书籍 数据库新增表: - book_details, book_progress, book_highlights, book_chapters - book_reviews, recommended_books, search_results MySQL 版本建表语句 schema_mysql.sql: - 18 张表的完整 DDL,含索引、字符集 utf8mb4、字段注释 - 适用于后续从 SQLite 迁移至 MySQL
851 lines
28 KiB
Python
851 lines
28 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
微信读书数据展示网站 - Flask 后端
|
|
提供 API 接口供前端查询本地 SQLite 数据库中的读书数据
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import sqlite3
|
|
import time
|
|
import urllib.request
|
|
from datetime import datetime
|
|
|
|
from flask import Flask, jsonify, render_template, request
|
|
|
|
app = Flask(__name__, static_folder="static", template_folder="templates")
|
|
|
|
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "weread.db")
|
|
|
|
WEREAD_API_URL = "https://i.weread.qq.com/api/agent/gateway"
|
|
WEREAD_API_KEY = os.environ.get("WEREAD_API_KEY", "")
|
|
WEREAD_SKILL_VERSION = "1.0.3"
|
|
|
|
|
|
def weread_api_call(api_name, **kwargs):
|
|
"""调用微信读书 API(按需获取数据时使用)"""
|
|
body = {"api_name": api_name, "skill_version": WEREAD_SKILL_VERSION}
|
|
body.update(kwargs)
|
|
req = urllib.request.Request(
|
|
WEREAD_API_URL,
|
|
data=json.dumps(body).encode("utf-8"),
|
|
headers={
|
|
"Authorization": f"Bearer {WEREAD_API_KEY}",
|
|
"Content-Type": "application/json",
|
|
},
|
|
method="POST",
|
|
)
|
|
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
return json.loads(resp.read().decode("utf-8"))
|
|
|
|
|
|
def ensure_tables(conn):
|
|
"""确保按需数据表存在"""
|
|
c = conn.cursor()
|
|
c.executescript("""
|
|
CREATE TABLE IF NOT EXISTS book_details (
|
|
book_id TEXT PRIMARY KEY,
|
|
title TEXT, author TEXT, translator TEXT, cover TEXT,
|
|
intro TEXT, category TEXT, publisher TEXT, publish_time TEXT,
|
|
isbn TEXT, word_count INTEGER, new_rating REAL,
|
|
new_rating_count INTEGER, fetched_at INTEGER
|
|
);
|
|
CREATE TABLE IF NOT EXISTS book_progress (
|
|
book_id TEXT PRIMARY KEY,
|
|
progress INTEGER, chapter_uid INTEGER, chapter_offset INTEGER,
|
|
update_time INTEGER, record_reading_time INTEGER,
|
|
finish_time INTEGER, is_start_reading INTEGER, fetched_at INTEGER
|
|
);
|
|
CREATE TABLE IF NOT EXISTS book_highlights (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
book_id TEXT, bookmark_id TEXT, chapter_uid INTEGER,
|
|
mark_text TEXT, create_time INTEGER, type INTEGER,
|
|
range_text TEXT, color_style INTEGER, fetched_at INTEGER
|
|
);
|
|
CREATE TABLE IF NOT EXISTS book_chapters (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
book_id TEXT, chapter_uid INTEGER, chapter_idx INTEGER,
|
|
title TEXT, UNIQUE(book_id, chapter_uid)
|
|
);
|
|
CREATE TABLE IF NOT EXISTS book_reviews (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
book_id TEXT, review_id TEXT, content TEXT,
|
|
create_time INTEGER, star INTEGER, chapter_name TEXT,
|
|
is_finish INTEGER, fetched_at INTEGER
|
|
);
|
|
CREATE TABLE IF NOT EXISTS recommended_books (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
book_id TEXT, title TEXT, author TEXT, cover TEXT,
|
|
intro TEXT, category TEXT, reason TEXT,
|
|
reading_count INTEGER, new_rating REAL,
|
|
new_rating_count INTEGER, rating_title TEXT, fetched_at INTEGER
|
|
);
|
|
CREATE TABLE IF NOT EXISTS search_results (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
keyword TEXT, book_id TEXT, title TEXT, author TEXT,
|
|
cover TEXT, intro TEXT, publisher TEXT, category TEXT,
|
|
reading_count INTEGER, new_rating REAL,
|
|
new_rating_count INTEGER, rating_title TEXT,
|
|
search_idx INTEGER, fetched_at INTEGER
|
|
);
|
|
""")
|
|
conn.commit()
|
|
|
|
|
|
def get_db():
|
|
conn = sqlite3.connect(DB_PATH)
|
|
conn.row_factory = sqlite3.Row
|
|
return conn
|
|
|
|
|
|
def ts_to_date(ts):
|
|
"""时间戳转日期字符串"""
|
|
if not ts or ts == 0:
|
|
return ""
|
|
return datetime.fromtimestamp(ts).strftime("%Y-%m-%d")
|
|
|
|
|
|
def ts_to_year(ts):
|
|
"""时间戳转年份"""
|
|
if not ts or ts == 0:
|
|
return ""
|
|
return datetime.fromtimestamp(ts).strftime("%Y")
|
|
|
|
|
|
def format_duration(seconds):
|
|
"""秒转可读时长"""
|
|
if not seconds:
|
|
return "0分钟"
|
|
hours = seconds // 3600
|
|
minutes = (seconds % 3600) // 60
|
|
if hours > 0:
|
|
return f"{hours}小时{minutes}分钟"
|
|
return f"{minutes}分钟"
|
|
|
|
|
|
@app.route("/")
|
|
def index():
|
|
return render_template("index.html")
|
|
|
|
|
|
@app.route("/api/overview")
|
|
def api_overview():
|
|
"""总览数据"""
|
|
conn = get_db()
|
|
c = conn.cursor()
|
|
|
|
# 总阅读统计
|
|
c.execute("SELECT * FROM reading_stats WHERE mode='overall' LIMIT 1")
|
|
overall = c.fetchone()
|
|
total_read_time = overall["total_read_time"] if overall else 0
|
|
read_days = overall["read_days"] if overall else 0
|
|
regist_time = overall["regist_time"] if overall else 0
|
|
|
|
# 今年统计
|
|
current_year = str(time.localtime().tm_year)
|
|
c.execute("SELECT * FROM reading_stats WHERE mode='annually' LIMIT 1")
|
|
annual = c.fetchone()
|
|
year_read_time = annual["total_read_time"] if annual else 0
|
|
year_read_days = annual["read_days"] if annual else 0
|
|
|
|
# 书架数量
|
|
c.execute("SELECT COUNT(*) as cnt FROM books")
|
|
shelf_count = c.fetchone()["cnt"]
|
|
|
|
# 读完数量
|
|
c.execute("SELECT COUNT(*) as cnt FROM books WHERE finish_reading=1")
|
|
finished_count = c.fetchone()["cnt"]
|
|
|
|
# 笔记统计
|
|
c.execute("SELECT COUNT(*) as cnt FROM notebooks")
|
|
notebook_count = c.fetchone()["cnt"]
|
|
c.execute("SELECT SUM(review_count + note_count + bookmark_count) as cnt FROM notebooks")
|
|
total_notes = c.fetchone()["cnt"] or 0
|
|
|
|
# 最近阅读时间
|
|
c.execute("SELECT MAX(read_update_time) as max_ts FROM books")
|
|
last_read_ts = c.fetchone()["max_ts"] or 0
|
|
|
|
# 采集时间
|
|
c.execute("SELECT value FROM fetch_meta WHERE key='last_fetch_time'")
|
|
fetch_row = c.fetchone()
|
|
fetch_time = fetch_row["value"] if fetch_row else ""
|
|
|
|
conn.close()
|
|
|
|
return jsonify({
|
|
"total_read_time": total_read_time,
|
|
"total_read_time_str": format_duration(total_read_time),
|
|
"read_days": read_days,
|
|
"year_read_time": year_read_time,
|
|
"year_read_time_str": format_duration(year_read_time),
|
|
"year_read_days": year_read_days,
|
|
"shelf_count": shelf_count,
|
|
"finished_count": finished_count,
|
|
"notebook_count": notebook_count,
|
|
"total_notes": total_notes,
|
|
"regist_time": ts_to_date(regist_time),
|
|
"last_read_time": ts_to_date(last_read_ts),
|
|
"fetch_time": fetch_time,
|
|
})
|
|
|
|
|
|
@app.route("/api/yearly-trend")
|
|
def api_yearly_trend():
|
|
"""年度阅读时长时间序列"""
|
|
conn = get_db()
|
|
c = conn.cursor()
|
|
c.execute(
|
|
"SELECT bucket_timestamp, duration FROM read_times WHERE mode='overall' ORDER BY bucket_timestamp"
|
|
)
|
|
rows = c.fetchall()
|
|
conn.close()
|
|
|
|
data = []
|
|
for row in rows:
|
|
year = ts_to_year(row["bucket_timestamp"])
|
|
hours = round(row["duration"] / 3600, 1)
|
|
data.append({"year": year, "hours": hours, "seconds": row["duration"]})
|
|
|
|
return jsonify(data)
|
|
|
|
|
|
@app.route("/api/top-books")
|
|
def api_top_books():
|
|
"""阅读时长最久的书籍"""
|
|
conn = get_db()
|
|
c = conn.cursor()
|
|
c.execute(
|
|
"""SELECT * FROM read_longest WHERE mode='overall'
|
|
ORDER BY read_time DESC LIMIT 20"""
|
|
)
|
|
rows = c.fetchall()
|
|
conn.close()
|
|
|
|
books = []
|
|
for row in rows:
|
|
tags = json.loads(row["tags"]) if row["tags"] else []
|
|
books.append({
|
|
"book_id": row["book_id"],
|
|
"title": row["title"],
|
|
"author": row["author"],
|
|
"cover": row["cover"],
|
|
"read_time": row["read_time"],
|
|
"read_time_str": format_duration(row["read_time"]),
|
|
"tags": tags,
|
|
})
|
|
|
|
return jsonify(books)
|
|
|
|
|
|
@app.route("/api/categories")
|
|
def api_categories():
|
|
"""偏好分类"""
|
|
conn = get_db()
|
|
c = conn.cursor()
|
|
c.execute(
|
|
"""SELECT * FROM prefer_categories WHERE mode='overall'
|
|
AND reading_time > 0
|
|
ORDER BY reading_time DESC"""
|
|
)
|
|
rows = c.fetchall()
|
|
conn.close()
|
|
|
|
cats = []
|
|
for row in rows:
|
|
cats.append({
|
|
"category_title": row["category_title"],
|
|
"parent_category_title": row["parent_category_title"],
|
|
"reading_time": row["reading_time"],
|
|
"reading_time_str": format_duration(row["reading_time"]),
|
|
"reading_count": row["reading_count"],
|
|
"val": row["val"],
|
|
})
|
|
|
|
return jsonify(cats)
|
|
|
|
|
|
@app.route("/api/authors")
|
|
def api_authors():
|
|
"""偏好作者"""
|
|
conn = get_db()
|
|
c = conn.cursor()
|
|
c.execute(
|
|
"""SELECT * FROM prefer_authors WHERE mode='overall'
|
|
ORDER BY count DESC LIMIT 20"""
|
|
)
|
|
rows = c.fetchall()
|
|
conn.close()
|
|
|
|
authors = []
|
|
for row in rows:
|
|
authors.append({
|
|
"name": row["name"],
|
|
"count": row["count"],
|
|
"read_time": row["read_time"],
|
|
})
|
|
|
|
return jsonify(authors)
|
|
|
|
|
|
@app.route("/api/time-distribution")
|
|
def api_time_distribution():
|
|
"""24小时阅读时段分布"""
|
|
conn = get_db()
|
|
c = conn.cursor()
|
|
c.execute(
|
|
"""SELECT hour_index, SUM(duration) as total
|
|
FROM prefer_time WHERE mode='overall'
|
|
GROUP BY hour_index ORDER BY hour_index"""
|
|
)
|
|
rows = c.fetchall()
|
|
conn.close()
|
|
|
|
dist = []
|
|
for row in rows:
|
|
dist.append({
|
|
"hour": row["hour_index"],
|
|
"duration": row["total"],
|
|
"duration_min": round(row["total"] / 60, 1),
|
|
})
|
|
|
|
return jsonify(dist)
|
|
|
|
|
|
@app.route("/api/shelf")
|
|
def api_shelf():
|
|
"""书架列表"""
|
|
page = int(request.args.get("page", 1))
|
|
per_page = int(request.args.get("per_page", 60))
|
|
category = request.args.get("category", "")
|
|
finished_only = request.args.get("finished", "0") == "1"
|
|
|
|
offset = (page - 1) * per_page
|
|
conn = get_db()
|
|
c = conn.cursor()
|
|
|
|
where_clauses = []
|
|
params = []
|
|
if category:
|
|
where_clauses.append("category LIKE ?")
|
|
params.append(f"%{category}%")
|
|
if finished_only:
|
|
where_clauses.append("finish_reading=1")
|
|
|
|
where_sql = " WHERE " + " AND ".join(where_clauses) if where_clauses else ""
|
|
|
|
c.execute(f"SELECT COUNT(*) as cnt FROM books{where_sql}", params)
|
|
total = c.fetchone()["cnt"]
|
|
|
|
c.execute(
|
|
f"SELECT * FROM books{where_sql} ORDER BY is_top DESC, read_update_time DESC LIMIT ? OFFSET ?",
|
|
params + [per_page, offset],
|
|
)
|
|
rows = c.fetchall()
|
|
|
|
# 获取分类列表
|
|
c.execute("SELECT DISTINCT category FROM books WHERE category != '' ORDER BY category")
|
|
categories = [r["category"] for r in c.fetchall()]
|
|
|
|
conn.close()
|
|
|
|
books = []
|
|
for row in rows:
|
|
books.append({
|
|
"book_id": row["book_id"],
|
|
"title": row["title"],
|
|
"author": row["author"],
|
|
"cover": row["cover"],
|
|
"category": row["category"],
|
|
"finish_reading": row["finish_reading"],
|
|
"read_update_time": ts_to_date(row["read_update_time"]),
|
|
"secret": row["secret"],
|
|
"is_top": row["is_top"],
|
|
"deep_link": row["deep_link"],
|
|
})
|
|
|
|
return jsonify({
|
|
"books": books,
|
|
"total": total,
|
|
"page": page,
|
|
"per_page": per_page,
|
|
"total_pages": (total + per_page - 1) // per_page,
|
|
"categories": categories,
|
|
})
|
|
|
|
|
|
@app.route("/api/notebooks")
|
|
def api_notebooks():
|
|
"""笔记本概览"""
|
|
conn = get_db()
|
|
c = conn.cursor()
|
|
c.execute(
|
|
"""SELECT * FROM notebooks
|
|
ORDER BY (review_count + note_count + bookmark_count) DESC"""
|
|
)
|
|
rows = c.fetchall()
|
|
conn.close()
|
|
|
|
notebooks = []
|
|
for row in rows:
|
|
total = row["review_count"] + row["note_count"] + row["bookmark_count"]
|
|
notebooks.append({
|
|
"book_id": row["book_id"],
|
|
"title": row["title"],
|
|
"author": row["author"],
|
|
"cover": row["cover"],
|
|
"review_count": row["review_count"],
|
|
"note_count": row["note_count"],
|
|
"bookmark_count": row["bookmark_count"],
|
|
"total_notes": total,
|
|
"reading_progress": row["reading_progress"],
|
|
"marked_status": row["marked_status"],
|
|
})
|
|
|
|
return jsonify(notebooks)
|
|
|
|
|
|
@app.route("/api/read-stats")
|
|
def api_read_stats():
|
|
"""阅读统计摘要项"""
|
|
conn = get_db()
|
|
c = conn.cursor()
|
|
c.execute("SELECT * FROM read_stat_items WHERE mode='overall'")
|
|
rows = c.fetchall()
|
|
conn.close()
|
|
|
|
stats = []
|
|
for row in rows:
|
|
stats.append({
|
|
"stat": row["stat_name"],
|
|
"counts": row["counts"],
|
|
"scheme": row["scheme"],
|
|
})
|
|
|
|
return jsonify(stats)
|
|
|
|
|
|
@app.route("/api/archives")
|
|
def api_archives():
|
|
"""书单"""
|
|
conn = get_db()
|
|
c = conn.cursor()
|
|
c.execute("SELECT * FROM archives")
|
|
rows = c.fetchall()
|
|
conn.close()
|
|
|
|
archives = []
|
|
for row in rows:
|
|
book_ids = json.loads(row["book_ids"]) if row["book_ids"] else []
|
|
archives.append({
|
|
"name": row["name"],
|
|
"book_count": len(book_ids),
|
|
"book_ids": book_ids,
|
|
})
|
|
|
|
return jsonify(archives)
|
|
|
|
|
|
# ===== 按需获取:书籍详情 =====
|
|
|
|
@app.route("/api/book/detail")
|
|
def api_book_detail():
|
|
"""书籍详情:实时调微信读书 API 获取,入库后返回"""
|
|
book_id = request.args.get("book_id", "")
|
|
if not book_id:
|
|
return jsonify({"error": "book_id is required"}), 400
|
|
|
|
conn = get_db()
|
|
ensure_tables(conn)
|
|
c = conn.cursor()
|
|
now = int(time.time())
|
|
|
|
# 1. 获取书籍信息
|
|
try:
|
|
info = weread_api_call("/book/info", bookId=book_id)
|
|
c.execute(
|
|
"""INSERT OR REPLACE INTO book_details
|
|
(book_id, title, author, translator, cover, intro, category,
|
|
publisher, publish_time, isbn, word_count, new_rating,
|
|
new_rating_count, fetched_at)
|
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
|
(
|
|
book_id,
|
|
info.get("title", ""),
|
|
info.get("author", ""),
|
|
info.get("translator", ""),
|
|
info.get("cover", ""),
|
|
info.get("intro", ""),
|
|
info.get("category", ""),
|
|
info.get("publisher", ""),
|
|
info.get("publishTime", ""),
|
|
info.get("isbn", ""),
|
|
info.get("wordCount", 0),
|
|
info.get("newRating", 0),
|
|
info.get("newRatingCount", 0),
|
|
now,
|
|
),
|
|
)
|
|
conn.commit()
|
|
except Exception as e:
|
|
# API 调用失败时尝试从数据库读取缓存
|
|
c.execute("SELECT * FROM book_details WHERE book_id=?", (book_id,))
|
|
cached = c.fetchone()
|
|
if not cached:
|
|
return jsonify({"error": f"获取书籍信息失败: {str(e)}"}), 500
|
|
|
|
# 2. 获取阅读进度
|
|
progress_data = {}
|
|
try:
|
|
prog = weread_api_call("/book/getprogress", bookId=book_id)
|
|
book_prog = prog.get("book", {})
|
|
c.execute(
|
|
"""INSERT OR REPLACE INTO book_progress
|
|
(book_id, progress, chapter_uid, chapter_offset, update_time,
|
|
record_reading_time, finish_time, is_start_reading, fetched_at)
|
|
VALUES (?,?,?,?,?,?,?,?,?)""",
|
|
(
|
|
book_id,
|
|
book_prog.get("progress", 0),
|
|
book_prog.get("chapterUid", 0),
|
|
book_prog.get("chapterOffset", 0),
|
|
book_prog.get("updateTime", 0),
|
|
book_prog.get("recordReadingTime", 0),
|
|
book_prog.get("finishTime", 0),
|
|
book_prog.get("isStartReading", 0),
|
|
now,
|
|
),
|
|
)
|
|
conn.commit()
|
|
progress_data = {
|
|
"progress": book_prog.get("progress", 0),
|
|
"record_reading_time": book_prog.get("recordReadingTime", 0),
|
|
"record_reading_time_str": format_duration(book_prog.get("recordReadingTime", 0)),
|
|
"update_time": ts_to_date(book_prog.get("updateTime", 0)),
|
|
"finish_time": ts_to_date(book_prog.get("finishTime", 0)),
|
|
"is_start_reading": book_prog.get("isStartReading", 0),
|
|
}
|
|
except Exception:
|
|
c.execute("SELECT * FROM book_progress WHERE book_id=?", (book_id,))
|
|
cached = c.fetchone()
|
|
if cached:
|
|
progress_data = {
|
|
"progress": cached["progress"],
|
|
"record_reading_time": cached["record_reading_time"],
|
|
"record_reading_time_str": format_duration(cached["record_reading_time"]),
|
|
"update_time": ts_to_date(cached["update_time"]),
|
|
"finish_time": ts_to_date(cached["finish_time"]),
|
|
"is_start_reading": cached["is_start_reading"],
|
|
}
|
|
|
|
# 3. 组装返回
|
|
c.execute("SELECT * FROM book_details WHERE book_id=?", (book_id,))
|
|
detail = c.fetchone()
|
|
conn.close()
|
|
|
|
if not detail:
|
|
return jsonify({"error": "数据获取失败"}), 500
|
|
|
|
return jsonify({
|
|
"book_id": book_id,
|
|
"title": detail["title"],
|
|
"author": detail["author"],
|
|
"translator": detail["translator"],
|
|
"cover": detail["cover"],
|
|
"intro": detail["intro"],
|
|
"category": detail["category"],
|
|
"publisher": detail["publisher"],
|
|
"publish_time": detail["publish_time"],
|
|
"isbn": detail["isbn"],
|
|
"word_count": detail["word_count"],
|
|
"new_rating": detail["new_rating"],
|
|
"new_rating_count": detail["new_rating_count"],
|
|
"progress": progress_data,
|
|
"deep_link": f"weread://reading?bId={book_id}",
|
|
})
|
|
|
|
|
|
# ===== 按需获取:笔记详情 =====
|
|
|
|
@app.route("/api/book/notes")
|
|
def api_book_notes():
|
|
"""笔记详情:实时调微信读书 API 获取划线+想法,入库后返回"""
|
|
book_id = request.args.get("book_id", "")
|
|
if not book_id:
|
|
return jsonify({"error": "book_id is required"}), 400
|
|
|
|
conn = get_db()
|
|
ensure_tables(conn)
|
|
c = conn.cursor()
|
|
now = int(time.time())
|
|
|
|
# 1. 获取划线
|
|
highlights = []
|
|
chapters = {}
|
|
try:
|
|
data = weread_api_call("/book/bookmarklist", bookId=book_id)
|
|
# 保存章节信息
|
|
for ch in data.get("chapters", []):
|
|
c.execute(
|
|
"""INSERT OR REPLACE INTO book_chapters
|
|
(book_id, chapter_uid, chapter_idx, title)
|
|
VALUES (?,?,?,?)""",
|
|
(book_id, ch.get("chapterUid", 0), ch.get("chapterIdx", 0), ch.get("title", "")),
|
|
)
|
|
chapters[ch.get("chapterUid", 0)] = ch.get("title", "")
|
|
|
|
# 保存划线
|
|
c.execute("DELETE FROM book_highlights WHERE book_id=?", (book_id,))
|
|
for bm in data.get("updated", []):
|
|
c.execute(
|
|
"""INSERT INTO book_highlights
|
|
(book_id, bookmark_id, chapter_uid, mark_text, create_time,
|
|
type, range_text, color_style, fetched_at)
|
|
VALUES (?,?,?,?,?,?,?,?,?)""",
|
|
(
|
|
book_id,
|
|
bm.get("bookmarkId", ""),
|
|
bm.get("chapterUid", 0),
|
|
bm.get("markText", ""),
|
|
bm.get("createTime", 0),
|
|
bm.get("type", 0),
|
|
bm.get("range", ""),
|
|
bm.get("colorStyle", 0),
|
|
now,
|
|
),
|
|
)
|
|
highlights.append({
|
|
"bookmark_id": bm.get("bookmarkId", ""),
|
|
"chapter_uid": bm.get("chapterUid", 0),
|
|
"chapter_title": chapters.get(bm.get("chapterUid", 0), ""),
|
|
"mark_text": bm.get("markText", ""),
|
|
"create_time": ts_to_date(bm.get("createTime", 0)),
|
|
"range": bm.get("range", ""),
|
|
})
|
|
conn.commit()
|
|
except Exception as e:
|
|
# 从数据库读取缓存
|
|
c.execute("SELECT * FROM book_highlights WHERE book_id=? ORDER BY create_time", (book_id,))
|
|
for row in c.fetchall():
|
|
c2 = conn.cursor()
|
|
c2.execute("SELECT title FROM book_chapters WHERE book_id=? AND chapter_uid=?", (book_id, row["chapter_uid"]))
|
|
ch_row = c2.fetchone()
|
|
highlights.append({
|
|
"bookmark_id": row["bookmark_id"],
|
|
"chapter_uid": row["chapter_uid"],
|
|
"chapter_title": ch_row["title"] if ch_row else "",
|
|
"mark_text": row["mark_text"],
|
|
"create_time": ts_to_date(row["create_time"]),
|
|
"range": row["range_text"],
|
|
})
|
|
|
|
# 2. 获取想法/点评
|
|
reviews = []
|
|
try:
|
|
rev_data = weread_api_call("/review/list/mine", bookid=book_id, count=100)
|
|
c.execute("DELETE FROM book_reviews WHERE book_id=?", (book_id,))
|
|
for item in rev_data.get("reviews", []):
|
|
rev = item.get("review", {})
|
|
c.execute(
|
|
"""INSERT INTO book_reviews
|
|
(book_id, review_id, content, create_time, star,
|
|
chapter_name, is_finish, fetched_at)
|
|
VALUES (?,?,?,?,?,?,?,?)""",
|
|
(
|
|
book_id,
|
|
rev.get("reviewId", ""),
|
|
rev.get("content", ""),
|
|
rev.get("createTime", 0),
|
|
rev.get("star", 0),
|
|
rev.get("chapterName", ""),
|
|
rev.get("isFinish", 0),
|
|
now,
|
|
),
|
|
)
|
|
reviews.append({
|
|
"review_id": rev.get("reviewId", ""),
|
|
"content": rev.get("content", ""),
|
|
"create_time": ts_to_date(rev.get("createTime", 0)),
|
|
"star": rev.get("star", 0),
|
|
"chapter_name": rev.get("chapterName", ""),
|
|
"is_finish": rev.get("isFinish", 0),
|
|
})
|
|
conn.commit()
|
|
except Exception:
|
|
c.execute("SELECT * FROM book_reviews WHERE book_id=? ORDER BY create_time DESC", (book_id,))
|
|
for row in c.fetchall():
|
|
reviews.append({
|
|
"review_id": row["review_id"],
|
|
"content": row["content"],
|
|
"create_time": ts_to_date(row["create_time"]),
|
|
"star": row["star"],
|
|
"chapter_name": row["chapter_name"],
|
|
"is_finish": row["is_finish"],
|
|
})
|
|
|
|
conn.close()
|
|
|
|
# 按章节分组划线
|
|
grouped_highlights = {}
|
|
for h in highlights:
|
|
ch_title = h["chapter_title"] or f"章节{h['chapter_uid']}"
|
|
if ch_title not in grouped_highlights:
|
|
grouped_highlights[ch_title] = []
|
|
grouped_highlights[ch_title].append(h)
|
|
|
|
return jsonify({
|
|
"book_id": book_id,
|
|
"highlights": highlights,
|
|
"highlight_count": len(highlights),
|
|
"grouped_highlights": grouped_highlights,
|
|
"reviews": reviews,
|
|
"review_count": len(reviews),
|
|
})
|
|
|
|
|
|
# ===== 推荐书籍 =====
|
|
|
|
@app.route("/api/recommend")
|
|
def api_recommend():
|
|
"""个性化推荐:实时调微信读书 API,入库后返回"""
|
|
max_idx = int(request.args.get("maxIdx", 0))
|
|
conn = get_db()
|
|
ensure_tables(conn)
|
|
c = conn.cursor()
|
|
now = int(time.time())
|
|
|
|
try:
|
|
data = weread_api_call("/book/recommend", count=12, maxIdx=max_idx)
|
|
books = data.get("books", [])
|
|
for book in books:
|
|
rating_detail = book.get("newRatingDetail", {})
|
|
c.execute(
|
|
"""INSERT OR REPLACE INTO recommended_books
|
|
(book_id, title, author, cover, intro, category, reason,
|
|
reading_count, new_rating, new_rating_count, rating_title, fetched_at)
|
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)""",
|
|
(
|
|
book.get("bookId", ""),
|
|
book.get("title", ""),
|
|
book.get("author", ""),
|
|
book.get("cover", ""),
|
|
book.get("intro", ""),
|
|
book.get("category", ""),
|
|
book.get("reason", ""),
|
|
book.get("readingCount", 0),
|
|
book.get("newRating", 0),
|
|
book.get("newRatingCount", 0),
|
|
rating_detail.get("title", "") if isinstance(rating_detail, dict) else "",
|
|
now,
|
|
),
|
|
)
|
|
conn.commit()
|
|
except Exception as e:
|
|
return jsonify({"error": f"获取推荐失败: {str(e)}"}), 500
|
|
|
|
conn.close()
|
|
|
|
result = []
|
|
for book in books:
|
|
rating_detail = book.get("newRatingDetail", {})
|
|
result.append({
|
|
"book_id": book.get("bookId", ""),
|
|
"title": book.get("title", ""),
|
|
"author": book.get("author", ""),
|
|
"cover": book.get("cover", ""),
|
|
"intro": book.get("intro", ""),
|
|
"category": book.get("category", ""),
|
|
"reason": book.get("reason", ""),
|
|
"reading_count": book.get("readingCount", 0),
|
|
"new_rating": book.get("newRating", 0),
|
|
"new_rating_count": book.get("newRatingCount", 0),
|
|
"rating_title": rating_detail.get("title", "") if isinstance(rating_detail, dict) else "",
|
|
})
|
|
|
|
return jsonify({
|
|
"books": result,
|
|
"has_more": data.get("hasMore", 0),
|
|
"last_search_idx": books[-1].get("searchIdx", 0) if books else 0,
|
|
})
|
|
|
|
|
|
# ===== 搜索书籍 =====
|
|
|
|
@app.route("/api/search")
|
|
def api_search():
|
|
"""搜索书籍:实时调微信读书 API,入库后返回"""
|
|
keyword = request.args.get("keyword", "").strip()
|
|
# 修复 Flask URL 参数中文编码问题
|
|
try:
|
|
keyword = keyword.encode("latin-1").decode("utf-8")
|
|
except (UnicodeDecodeError, UnicodeEncodeError):
|
|
pass
|
|
if not keyword:
|
|
return jsonify({"error": "keyword is required"}), 400
|
|
|
|
max_idx = int(request.args.get("maxIdx", 0))
|
|
conn = get_db()
|
|
ensure_tables(conn)
|
|
c = conn.cursor()
|
|
now = int(time.time())
|
|
|
|
try:
|
|
data = weread_api_call("/store/search", keyword=keyword, scope=10, maxIdx=max_idx)
|
|
results = data.get("results", [])
|
|
books = []
|
|
for group in results:
|
|
for item in group.get("books", []):
|
|
bi = item.get("bookInfo", {})
|
|
rating_detail = item.get("newRatingDetail", {})
|
|
c.execute(
|
|
"""INSERT OR REPLACE INTO search_results
|
|
(keyword, book_id, title, author, cover, intro, publisher,
|
|
category, reading_count, new_rating, new_rating_count,
|
|
rating_title, search_idx, fetched_at)
|
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
|
(
|
|
keyword,
|
|
bi.get("bookId", ""),
|
|
bi.get("title", ""),
|
|
bi.get("author", ""),
|
|
bi.get("cover", ""),
|
|
bi.get("intro", ""),
|
|
bi.get("publisher", ""),
|
|
bi.get("category", ""),
|
|
item.get("readingCount", 0),
|
|
item.get("newRating", 0),
|
|
item.get("newRatingCount", 0),
|
|
rating_detail.get("title", "") if isinstance(rating_detail, dict) else "",
|
|
item.get("searchIdx", 0),
|
|
now,
|
|
),
|
|
)
|
|
books.append({
|
|
"book_id": bi.get("bookId", ""),
|
|
"title": bi.get("title", ""),
|
|
"author": bi.get("author", ""),
|
|
"cover": bi.get("cover", ""),
|
|
"intro": bi.get("intro", ""),
|
|
"publisher": bi.get("publisher", ""),
|
|
"category": bi.get("category", ""),
|
|
"reading_count": item.get("readingCount", 0),
|
|
"new_rating": item.get("newRating", 0),
|
|
"new_rating_count": item.get("newRatingCount", 0),
|
|
"rating_title": rating_detail.get("title", "") if isinstance(rating_detail, dict) else "",
|
|
})
|
|
conn.commit()
|
|
except Exception as e:
|
|
return jsonify({"error": f"搜索失败: {str(e)}"}), 500
|
|
|
|
conn.close()
|
|
|
|
return jsonify({
|
|
"keyword": keyword,
|
|
"books": books,
|
|
"has_more": data.get("hasMore", 0),
|
|
})
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app.run(host="0.0.0.0", port=8080, debug=False)
|