import os import io import zipfile from datetime import datetime from flask import request, jsonify, render_template, redirect, url_for, send_file, current_app from .common import main_bp, get_db_connection, login_required, current_user from database import get_folder_tree, get_all_descendant_folder_ids from strings import METHOD_POST, METHOD_DELETE @main_bp.route('/pdf_manager') @main_bp.route('/pdf_manager/browse/') @login_required def pdf_manager(folder_path=''): conn = get_db_connection() view_mode, search_query, is_recursive = request.args.get('view', 'default'), request.args.get('search', ''), request.args.get('recursive') == 'true' query_params, base_query, where_clauses = [current_user.id], 'SELECT * FROM generated_pdfs WHERE user_id = ?', [] if search_query: where_clauses.append('(subject LIKE ? OR tags LIKE ? OR notes LIKE ?)') search_term = f'%{search_query}%'; query_params.extend([search_term, search_term, search_term]) all_view, folder_id, subfolders, breadcrumbs = view_mode == 'all', None, [], [] if not all_view: if folder_path: parent_id = None for i, part in enumerate(folder_path.split('/')): res = conn.execute("SELECT id FROM folders WHERE name = ? AND user_id = ? AND (parent_id = ? OR (? IS NULL AND parent_id IS NULL))", (part, current_user.id, parent_id, parent_id)).fetchone() if not res: return redirect(url_for('main.pdf_manager')) parent_id = res['id']; breadcrumbs.append({'name': part, 'path': '/'.join(folder_path.split('/')[:i+1])}) folder_id = parent_id if is_recursive and search_query: if folder_id: descendant_ids = get_all_descendant_folder_ids(conn, folder_id, current_user.id) all_f_ids = [folder_id] + descendant_ids where_clauses.append(f"folder_id IN ({', '.join('?' * len(all_f_ids))})"); query_params.extend(all_f_ids) else: if folder_id: where_clauses.append('folder_id = ?'); query_params.append(folder_id) else: where_clauses.append('folder_id IS NULL') if folder_id: subfolders = conn.execute('SELECT * FROM folders WHERE parent_id = ? AND user_id = ? ORDER BY name', (folder_id, current_user.id)).fetchall() else: subfolders = conn.execute('SELECT * FROM folders WHERE parent_id IS NULL AND user_id = ? ORDER BY name', (current_user.id,)).fetchall() if where_clauses: base_query += ' AND ' + ' AND '.join(where_clauses) base_query += ' ORDER BY created_at DESC' pdfs = conn.execute(base_query, query_params).fetchall() pdfs_list = [dict(row) for row in pdfs] subfolders_list = [dict(row) for row in subfolders] for p in pdfs_list: if isinstance(p['created_at'], str): try: p['created_at'] = datetime.strptime(p['created_at'], '%Y-%m-%d %H:%M:%S') except ValueError: pass for f in subfolders_list: if isinstance(f['created_at'], str): try: f['created_at'] = datetime.strptime(f['created_at'], '%Y-%m-%d %H:%M:%S') except ValueError: pass tree = get_folder_tree(user_id=current_user.id); conn.close() return render_template('pdf_manager.html', pdfs=pdfs_list, subfolders=subfolders_list, current_folder_id=folder_id, breadcrumbs=breadcrumbs, all_view=all_view, folder_tree=tree, search_query=search_query, recursive=is_recursive) @main_bp.route('/get_pdf_details/') @login_required def get_pdf_details(pdf_id): conn = get_db_connection() current_app.logger.info(f"Fetching details for PDF {pdf_id} for User {current_user.id}") pdf = conn.execute('SELECT * FROM generated_pdfs WHERE id = ? AND user_id = ?', (pdf_id, current_user.id)).fetchone() conn.close() if pdf: return jsonify(dict(pdf)) current_app.logger.warning(f"PDF {pdf_id} not found for User {current_user.id}") return jsonify({'error': 'PDF not found'}), 404 @main_bp.route('/update_pdf_details/', methods=[METHOD_POST]) @login_required def update_pdf_details(pdf_id): data = request.json try: conn = get_db_connection() pdf_owner = conn.execute('SELECT user_id FROM generated_pdfs WHERE id = ?', (pdf_id,)).fetchone() if not pdf_owner or pdf_owner['user_id'] != current_user.id: conn.close(); return jsonify({'error': 'Unauthorized'}), 403 conn.execute('UPDATE generated_pdfs SET subject = ?, tags = ?, notes = ? WHERE id = ?', (data.get('subject'), data.get('tags'), data.get('notes'), pdf_id)) conn.commit(); conn.close(); return jsonify({'success': True}) except Exception as e: return jsonify({'error': str(e)}), 500 @main_bp.route('/rename_item', methods=[METHOD_POST]) @login_required def rename_item(): data = request.json item_type, item_id, new_name = data.get('item_type'), data.get('item_id'), data.get('new_name') if not all([item_type, item_id, new_name]): return jsonify({'error': 'Missing parameters'}), 400 conn = get_db_connection() if item_type == 'folder': owner = conn.execute('SELECT user_id FROM folders WHERE id = ?', (item_id,)).fetchone() if not owner or owner['user_id'] != current_user.id: conn.close(); return jsonify({'error': 'Unauthorized'}), 403 conn.execute('UPDATE folders SET name = ? WHERE id = ?', (new_name, item_id)) elif item_type == 'pdf': from werkzeug.utils import secure_filename owner = conn.execute('SELECT user_id, filename FROM generated_pdfs WHERE id = ?', (item_id,)).fetchone() if not owner or owner['user_id'] != current_user.id: conn.close(); return jsonify({'error': 'Unauthorized'}), 403 old_name = owner['filename'] if not new_name.lower().endswith('.pdf'): new_name += '.pdf' new_fname = secure_filename(new_name) old_path, new_path = os.path.join(current_app.config['OUTPUT_FOLDER'], old_name), os.path.join(current_app.config['OUTPUT_FOLDER'], new_fname) if os.path.exists(new_path): conn.close(); return jsonify({'error': 'Exists'}), 400 try: os.rename(old_path, new_path); conn.execute('UPDATE generated_pdfs SET filename = ? WHERE id = ?', (new_fname, item_id)) except OSError as e: conn.close(); return jsonify({'error': str(e)}), 500 else: conn.close(); return jsonify({'error': 'Invalid'}), 400 conn.commit(); conn.close(); return jsonify({'success': True}) @main_bp.route('/delete_folder/', methods=[METHOD_DELETE]) @login_required def delete_folder(folder_id): conn = get_db_connection() owner = conn.execute('SELECT user_id FROM folders WHERE id = ?', (folder_id,)).fetchone() if not owner or owner['user_id'] != current_user.id: conn.close(); return jsonify({'error': 'Unauthorized'}), 403 f_ids = [folder_id] + get_all_descendant_folder_ids(conn, folder_id, current_user.id) placeholders = ', '.join('?' * len(f_ids)) pdfs = conn.execute(f'SELECT id, filename FROM generated_pdfs WHERE folder_id IN ({placeholders}) AND user_id = ?', (*f_ids, current_user.id)).fetchall() for p in pdfs: try: os.remove(os.path.join(current_app.config['OUTPUT_FOLDER'], p['filename'])) except OSError: pass p_ids = [p['id'] for p in pdfs] if p_ids: conn.execute(f"DELETE FROM generated_pdfs WHERE id IN ({','.join(map(str, p_ids))})") conn.execute(f"DELETE FROM folders WHERE id IN ({placeholders})", f_ids) conn.commit(); conn.close(); return jsonify({'success': True}) @main_bp.route('/delete_generated_pdf/', methods=[METHOD_DELETE]) @login_required def delete_generated_pdf(pdf_id): try: conn = get_db_connection() info = conn.execute('SELECT filename, user_id FROM generated_pdfs WHERE id = ?', (pdf_id,)).fetchone() if info and info['user_id'] == current_user.id: try: os.remove(os.path.join(current_app.config['OUTPUT_FOLDER'], info['filename'])) except OSError: pass conn.execute('DELETE FROM generated_pdfs WHERE id = ?', (pdf_id,)) conn.commit() conn.close(); return jsonify({'success': True}) except Exception as e: return jsonify({'error': str(e)}), 500 @main_bp.route('/toggle_persist_generated_pdf/', methods=[METHOD_POST]) @login_required def toggle_persist_generated_pdf(pdf_id): try: conn = get_db_connection() info = conn.execute('SELECT persist, session_id, user_id FROM generated_pdfs WHERE id = ?', (pdf_id,)).fetchone() if not info or info['user_id'] != current_user.id: conn.close(); return jsonify({'error': 'Unauthorized'}), 403 new_status = 1 - info['persist'] conn.execute('UPDATE generated_pdfs SET persist = ? WHERE id = ?', (new_status, pdf_id)) if info['session_id']: conn.execute('UPDATE sessions SET persist = ? WHERE id = ? AND user_id = ?', (new_status, info['session_id'], current_user.id)) conn.commit(); conn.close(); return jsonify({'success': True, 'status': 'persisted' if new_status == 1 else 'not_persisted'}) except Exception as e: conn.rollback(); conn.close(); return jsonify({'error': str(e)}), 500 @main_bp.route('/bulk_delete_pdfs', methods=[METHOD_POST]) @login_required def bulk_delete_pdfs(): ids = request.json.get('ids', []) if not ids: return jsonify({'error': 'None provided'}), 400 try: conn = get_db_connection(); placeholders = ','.join('?' for _ in ids) owned = conn.execute(f'SELECT id, filename FROM generated_pdfs WHERE id IN ({placeholders}) AND user_id = ?', (*ids, current_user.id)).fetchall() o_ids = [p['id'] for p in owned] if not o_ids: conn.close(); return jsonify({'success': True}) for p in owned: try: os.remove(os.path.join(current_app.config['OUTPUT_FOLDER'], p['filename'])) except OSError: pass conn.execute(f"DELETE FROM generated_pdfs WHERE id IN ({','.join('?' for _ in o_ids)})", o_ids) conn.commit(); conn.close(); return jsonify({'success': True}) except Exception as e: return jsonify({'error': str(e)}), 500 @main_bp.route('/bulk_toggle_persist', methods=[METHOD_POST]) @login_required def bulk_toggle_persist(): ids = request.json.get('ids', []) if not ids: return jsonify({'error': 'None provided'}), 400 try: conn = get_db_connection(); placeholders = ','.join('?' for _ in ids) owned = conn.execute(f'SELECT id, persist, session_id FROM generated_pdfs WHERE id IN ({placeholders}) AND user_id = ?', (*ids, current_user.id)).fetchall() for p in owned: new_s = 1 - p['persist'] conn.execute('UPDATE generated_pdfs SET persist = ? WHERE id = ?', (new_s, p['id'])) if p['session_id']: conn.execute('UPDATE sessions SET persist = ? WHERE id = ? AND user_id = ?', (new_s, p['session_id'], current_user.id)) conn.commit(); conn.close(); return jsonify({'success': True}) except Exception as e: conn.rollback(); conn.close(); return jsonify({'error': str(e)}), 500 @main_bp.route('/bulk_download_pdfs', methods=[METHOD_POST]) @login_required def bulk_download_pdfs(): import io, zipfile ids = request.json.get('ids', []) if not ids: return jsonify({'error': 'None provided'}), 400 memory_file = io.BytesIO() try: with zipfile.ZipFile(memory_file, 'w', zipfile.ZIP_DEFLATED) as zf: conn = get_db_connection(); placeholders = ','.join('?' for _ in ids) owned = conn.execute(f'SELECT filename FROM generated_pdfs WHERE id IN ({placeholders}) AND user_id = ?', (*ids, current_user.id)).fetchall() for p in owned: path = os.path.join(current_app.config['OUTPUT_FOLDER'], p['filename']) if os.path.exists(path): zf.write(path, os.path.basename(path)) conn.close() except Exception as e: return jsonify({'error': str(e)}), 500 memory_file.seek(0); return send_file(memory_file, mimetype='application/zip', as_attachment=True, download_name='pdfs.zip') @main_bp.route('/create_folder', methods=[METHOD_POST]) @login_required def create_folder(): data = request.json; name, parent_id = data.get('new_folder_name'), data.get('parent_id') if not name: return jsonify({'error': 'Name required'}), 400 try: conn = get_db_connection(); cursor = conn.cursor() cursor.execute("INSERT INTO folders (name, parent_id, user_id) VALUES (?, ?, ?)", (name, parent_id, current_user.id)) new_id = cursor.lastrowid; conn.commit(); conn.close() return jsonify({'success': True, 'id': new_id, 'name': name, 'parent_id': parent_id}) except Exception as e: return jsonify({'error': str(e)}), 500 @main_bp.route('/bulk_move_pdfs', methods=[METHOD_POST]) @login_required def bulk_move_pdfs(): data = request.json; ids, target_id = data.get('ids', []), data.get('target_folder_id') if not ids: return jsonify({'error': 'None provided'}), 400 try: conn = get_db_connection() if target_id: owner = conn.execute('SELECT user_id FROM folders WHERE id = ?', (target_id,)).fetchone() if not owner or owner['user_id'] != current_user.id: conn.close(); return jsonify({'error': 'Unauthorized'}), 403 placeholders = ', '.join('?' * len(ids)) conn.execute(f'UPDATE generated_pdfs SET folder_id = ? WHERE id IN ({placeholders}) AND user_id = ?', (target_id, *ids, current_user.id)) conn.commit(); conn.close(); return jsonify({'success': True}) except Exception as e: return jsonify({'error': str(e)}), 500 @main_bp.route('/get_all_subjects_and_tags') @login_required def get_all_subjects_and_tags(): conn = get_db_connection() subjects = [row['subject'] for row in conn.execute('SELECT DISTINCT subject FROM generated_pdfs WHERE subject IS NOT NULL AND user_id = ?', (current_user.id,)).fetchall()] tags_query = conn.execute("SELECT DISTINCT tags FROM generated_pdfs WHERE tags IS NOT NULL AND tags != '' AND user_id = ?", (current_user.id,)).fetchall() all_tags = set() for row in tags_query: for t in row['tags'].split(','): all_tags.add(t.strip()) conn.close(); return jsonify({'subjects': sorted(subjects), 'tags': sorted(list(all_tags))}) @main_bp.route('/get_metadata_suggestions') @login_required def get_metadata_suggestions(): conn = get_db_connection() subjects = [row['subject'] for row in conn.execute('SELECT DISTINCT subject FROM generated_pdfs WHERE subject IS NOT NULL AND user_id = ?', (current_user.id,)).fetchall()] tags_query = conn.execute("SELECT DISTINCT tags FROM generated_pdfs WHERE tags IS NOT NULL AND tags != '' AND user_id = ?", (current_user.id,)).fetchall() all_tags = set() for row in tags_query: for t in row['tags'].split(','): all_tags.add(t.strip()) conn.close(); return jsonify({'subjects': sorted(subjects), 'tags': sorted(list(all_tags))})