import os import json import requests from flask import Blueprint, render_template, send_file, redirect, url_for, flash from app import db from src.models import Issue, Article, Image from src.cover import generate_cover from src.epub_builder import build_epub import config issues_bp = Blueprint("issues", __name__) @issues_bp.route("/issues") def index(): issues = Issue.query.order_by(Issue.created_at.desc()).all() issue_data = [] for issue in issues: article_count = len(json.loads(issue.article_ids)) issue_data.append({ "issue": issue, "article_count": article_count, }) return render_template("issues.html", issues=issue_data) @issues_bp.route("/issues//download") def download(issue_id): issue = db.get_or_404(Issue, issue_id) if not os.path.exists(issue.epub_path): flash("ePub file not found.", "error") return redirect(url_for("issues.index")) return send_file( issue.epub_path, as_attachment=True, download_name=os.path.basename(issue.epub_path), ) @issues_bp.route("/issues//epub") def epub_file(issue_id): issue = db.get_or_404(Issue, issue_id) if not os.path.exists(issue.epub_path): flash("ePub file not found.", "error") return redirect(url_for("issues.index")) return send_file(issue.epub_path, mimetype="application/epub+zip") @issues_bp.route("/issues//cover") def cover_image(issue_id): issue = db.get_or_404(Issue, issue_id) if not issue.cover_path or not os.path.exists(issue.cover_path): flash("Cover image not found.", "error") return redirect(url_for("issues.index")) return send_file(issue.cover_path, mimetype="image/jpeg") @issues_bp.route("/issues//read") def read(issue_id): issue = db.get_or_404(Issue, issue_id) if not os.path.exists(issue.epub_path): flash("ePub file not found.", "error") return redirect(url_for("issues.index")) article_count = len(json.loads(issue.article_ids)) if issue.issue_type == "single_article": article_ids = json.loads(issue.article_ids) article = db.session.get(Article, article_ids[0]) if article_ids else None title = article.title if article else "Single Article" elif issue.issue_type == "multi_week": w1 = issue.week_start.isocalendar()[1] w2 = issue.week_end.isocalendar()[1] title = f"Weeks {w1}\u2013{w2}" else: title = f"Week {issue.week_start.isocalendar()[1]}" return render_template( "reader.html", issue=issue, title=title, article_count=article_count, ) @issues_bp.route("/issues//delete", methods=["POST"]) def delete(issue_id): issue = db.get_or_404(Issue, issue_id) if issue.epub_path and os.path.exists(issue.epub_path): os.remove(issue.epub_path) if issue.cover_path and os.path.exists(issue.cover_path): os.remove(issue.cover_path) db.session.delete(issue) db.session.commit() flash("Issue deleted.") return redirect(url_for("issues.index")) @issues_bp.route("/issues//regenerate", methods=["POST"]) def regenerate(issue_id): issue = db.get_or_404(Issue, issue_id) article_ids = json.loads(issue.article_ids) articles_for_issue = ( Article.query.filter(Article.id.in_(article_ids)) .order_by(Article.pub_date.asc()) .all() ) headlines = [ a.title for a in articles_for_issue if "Obituaries" not in json.loads(a.categories) ] image_paths = [] for a in articles_for_issue: if "Obituaries" in json.loads(a.categories): continue first_image = Image.query.filter_by(article_id=a.id).first() if first_image: image_paths.append(first_image.local_path) cover_method = issue.cover_method if cover_method == "ai": issue.cover_method = "mosaic" cover_method = "mosaic" try: cover_path = generate_cover( cover_method, config.ISSUES_DIR, issue.week_start, issue.week_end, headlines, image_paths ) epub_path = build_epub( issue.week_start, issue.week_end, article_ids, cover_path, config.ISSUES_DIR ) issue.cover_path = cover_path issue.epub_path = epub_path db.session.commit() flash("Issue regenerated successfully.") except Exception as e: flash(f"Regeneration failed: {e}", "error") return redirect(url_for("issues.index")) @issues_bp.route("/issues//push", methods=["POST"]) def push(issue_id): issue = db.get_or_404(Issue, issue_id) if not config.GRIMMORY_URL or not config.GRIMMORY_USERNAME or not config.GRIMMORY_PASSWORD: flash("Grimmory/Booklore integration is not configured.", "error") return redirect(url_for("issues.index")) if not issue.epub_path or not os.path.exists(issue.epub_path): flash("ePub file not found.", "error") return redirect(url_for("issues.index")) try: base_url = config.GRIMMORY_URL.rstrip('/') # 1. Authenticate to get a token login_resp = requests.post( f"{base_url}/api/v1/auth/login", json={"username": config.GRIMMORY_USERNAME, "password": config.GRIMMORY_PASSWORD}, timeout=10 ) if login_resp.status_code != 200: flash("Failed to authenticate with library. Check username/password.", "error") return redirect(url_for("issues.index")) login_data = login_resp.json() token = login_data.get("token") or login_data.get("accessToken") or login_data.get("accessToken_Internal") if not token and "data" in login_data and isinstance(login_data["data"], dict): token = login_data["data"].get("token") or login_data["data"].get("accessToken") if not token: flash("Failed to extract authentication token from library response.", "error") return redirect(url_for("issues.index")) # 2. Resolve Library ID and Path ID if configured data = {} if config.GRIMMORY_LIBRARY_ID: headers = {'Authorization': f'Bearer {token}'} lib_resp = requests.get(f"{base_url}/api/v1/libraries", headers=headers, timeout=10) if lib_resp.status_code == 200: libraries = lib_resp.json() target_lib = None # Try to find by ID first, then by name for lib in libraries: if str(lib.get('id')) == str(config.GRIMMORY_LIBRARY_ID) or lib.get('name', '').lower() == str(config.GRIMMORY_LIBRARY_ID).lower(): target_lib = lib break if target_lib: data['libraryId'] = target_lib['id'] if target_lib.get('paths') and len(target_lib['paths']) > 0: data['pathId'] = target_lib['paths'][0]['id'] else: flash(f"Library '{target_lib['name']}' has no paths configured.", "error") return redirect(url_for("issues.index")) else: flash(f"Could not find library matching '{config.GRIMMORY_LIBRARY_ID}'.", "error") return redirect(url_for("issues.index")) else: flash(f"Failed to fetch libraries from server. Status: {lib_resp.status_code}", "error") return redirect(url_for("issues.index")) # 3. Upload the file to the library files = { 'file': ( os.path.basename(issue.epub_path), open(issue.epub_path, 'rb'), 'application/epub+zip' ) } headers = {'Authorization': f'Bearer {token}'} response = requests.post( f"{base_url}/api/v1/files/upload", files=files, data=data, headers=headers, timeout=30 ) if response.status_code in (200, 201, 204): flash("Issue successfully pushed to library.") elif response.status_code == 409: flash("Issue was already pushed to the library previously.") else: flash(f"Failed to push issue. Status: {response.status_code}. {response.text}", "error") except requests.RequestException as e: flash(f"Error connecting to library server: {e}", "error") except Exception as e: flash(f"An unexpected error occurred: {e}", "error") return redirect(url_for("issues.index"))