蔵書管理システムスマホ版

# book_manager_simple.py (OCR/バーコード機能なし版)

import tkinter as tk

from tkinter import ttk, messagebox, scrolledtext, filedialog

import sqlite3

import requests

from bs4 import BeautifulSoup

from PIL import Image, ImageTk

import os

from datetime import datetime

class BookDatabase:

    """SQLiteデータベース管理クラス"""

    

    def __init__(self, db_path="book_manager.db"):

        self.db_path = db_path

        self.init_database()

    

    def get_connection(self):

        conn = sqlite3.connect(self.db_path)

        conn.row_factory = sqlite3.Row

        return conn

    

    def init_database(self):

        conn = self.get_connection()

        cur = conn.cursor()

        

        cur.execute("""

            CREATE TABLE IF NOT EXISTS books (

                id INTEGER PRIMARY KEY AUTOINCREMENT,

                isbn TEXT UNIQUE,

                title TEXT,

                image_path TEXT,

                amazon_rating REAL,

                amazon_review_count INTEGER,

                amazon_url TEXT,

                status TEXT DEFAULT 'owned',

                purchase_date TEXT,

                sell_date TEXT,

                sell_price INTEGER,

                notes TEXT,

                created_at TEXT DEFAULT CURRENT_TIMESTAMP,

                updated_at TEXT DEFAULT CURRENT_TIMESTAMP

            )

        """)

        

        cur.execute("""

            CREATE TABLE IF NOT EXISTS search_history (

                id INTEGER PRIMARY KEY AUTOINCREMENT,

                isbn TEXT,

                title TEXT,

                searched_at TEXT DEFAULT CURRENT_TIMESTAMP,

                found_in_db INTEGER

            )

        """)

        

        cur.execute("CREATE INDEX IF NOT EXISTS idx_isbn ON books(isbn)")

        cur.execute("CREATE INDEX IF NOT EXISTS idx_status ON books(status)")

        

        conn.commit()

        conn.close()

    

    def add_book(self, book_data):

        conn = self.get_connection()

        cur = conn.cursor()

        

        try:

            cur.execute("""

                INSERT INTO books (

                    isbn, title, image_path, amazon_rating, 

                    amazon_review_count, amazon_url, status, purchase_date

                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)

            """, (

                book_data.get('isbn'),

                book_data.get('title'),

                book_data.get('image_path'),

                book_data.get('amazon_rating'),

                book_data.get('amazon_review_count'),

                book_data.get('amazon_url'),

                'owned',

                datetime.now().strftime('%Y-%m-%d')

            ))

            

            book_id = cur.lastrowid

            conn.commit()

            return book_id

        except sqlite3.IntegrityError:

            return None

        finally:

            conn.close()

    

    def get_books(self, status=None):

        conn = self.get_connection()

        cur = conn.cursor()

        

        if status:

            cur.execute("SELECT * FROM books WHERE status = ? ORDER BY created_at DESC", (status,))

        else:

            cur.execute("SELECT * FROM books ORDER BY created_at DESC")

        

        books = [dict(row) for row in cur.fetchall()]

        conn.close()

        return books

    

    def update_book(self, book_id, updates):

        conn = self.get_connection()

        cur = conn.cursor()

        

        set_clause = ", ".join([f"{k} = ?" for k in updates.keys()])

        values = list(updates.values()) + [book_id]

        

        cur.execute(f"UPDATE books SET {set_clause} WHERE id = ?", values)

        conn.commit()

        conn.close()

    

    def check_duplicate(self, isbn=None, title=None):

        conn = self.get_connection()

        cur = conn.cursor()

        

        results = []

        

        if isbn:

            cur.execute("SELECT * FROM books WHERE isbn = ?", (isbn,))

            results.extend([dict(row) for row in cur.fetchall()])

        

        if title and not results:

            cur.execute("SELECT * FROM books WHERE title LIKE ?", (f"%{title}%",))

            results.extend([dict(row) for row in cur.fetchall()])

        

        cur.execute("""

            INSERT INTO search_history (isbn, title, found_in_db)

            VALUES (?, ?, ?)

        """, (isbn, title, len(results) > 0))

        

        conn.commit()

        conn.close()

        return results

    

    def get_low_rated_books(self, threshold=3.5):

        conn = self.get_connection()

        cur = conn.cursor()

        

        cur.execute("""

            SELECT * FROM books 

            WHERE amazon_rating IS NOT NULL 

            AND amazon_rating < ? 

            AND status = 'owned'

            ORDER BY amazon_rating ASC

        """, (threshold,))

        

        books = [dict(row) for row in cur.fetchall()]

        conn.close()

        return books

    

    def get_stats(self):

        conn = self.get_connection()

        cur = conn.cursor()

        

        cur.execute("""

            SELECT 

                status,

                COUNT(*) as count,

                AVG(amazon_rating) as avg_rating,

                SUM(CASE WHEN sell_price IS NOT NULL THEN sell_price ELSE 0 END) as total_sales

            FROM books

            GROUP BY status

        """)

        

        stats = [dict(row) for row in cur.fetchall()]

        conn.close()

        return stats

class AmazonScraper:

    @staticmethod

    def isbn13_to_isbn10(isbn13):

        if len(isbn13) != 13 or not isbn13.startswith('978'):

            return isbn13

        

        isbn10_base = isbn13[3:-1]

        check = sum((i + 1) * int(x) for i, x in enumerate(isbn10_base)) % 11

        check_digit = 'X' if check == 10 else str(check)

        return isbn10_base + check_digit

    

    @staticmethod

    def search_amazon(isbn):

        if not isbn:

            return {'rating': None, 'review_count': 0, 'url': None}

        

        try:

            isbn10 = AmazonScraper.isbn13_to_isbn10(isbn)

            url = f"https://www.amazon.co.jp/dp/{isbn10}"

            

            headers = {

                'User-Agent': 'Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36'

            }

            

            response = requests.get(url, headers=headers, timeout=15)

            

            if response.status_code != 200:

                return {'rating': None, 'review_count': 0, 'url': url}

            

            soup = BeautifulSoup(response.content, 'html.parser')

            

            rating = None

            review_count = 0

            

            rating_elem = soup.select_one('span.a-icon-alt')

            if rating_elem:

                rating_text = rating_elem.get_text()

                try:

                    rating = float(rating_text.split()[0].replace('5つ星のうち', ''))

                except:

                    pass

            

            review_elem = soup.select_one('#acrCustomerReviewText')

            if review_elem:

                review_text = review_elem.get_text()

                try:

                    review_count = int(''.join(filter(str.isdigit, review_text)))

                except:

                    pass

            

            return {'rating': rating, 'review_count': review_count, 'url': url}

        

        except Exception as e:

            print(f"Amazon検索エラー: {e}")

            return {'rating': None, 'review_count': 0, 'url': None}

class BookManagerApp:

    def __init__(self, root):

        self.root = root

        self.root.title("📚 蔵書管理 (簡易版)")

        self.root.geometry("400x700")

        

        self.db = BookDatabase()

        

        self.image_dir = "book_images"

        os.makedirs(self.image_dir, exist_ok=True)

        

        self.current_image_path = None

        self.current_books = []

        self.low_rated_books = []

        

        self.create_widgets()

        self.show_welcome()

    

    def show_welcome(self):

        stats = self.db.get_stats()

        total = sum(s['count'] for s in stats)

        messagebox.showinfo("起動", f"📚 蔵書管理システム\n登録済み: {total}冊")

    

    def create_widgets(self):

        self.notebook = ttk.Notebook(self.root)

        self.notebook.pack(fill='both', expand=True, padx=5, pady=5)

        

        self.create_register_tab()

        self.create_list_tab()

        self.create_sell_tab()

        self.create_check_tab()

        self.create_stats_tab()

    

    def create_register_tab(self):

        tab = ttk.Frame(self.notebook, padding=10)

        self.notebook.add(tab, text='📝 登録')

        

        ttk.Label(tab, text="ISBN (13桁):").pack(anchor='w', pady=(5,0))

        self.isbn_entry = ttk.Entry(tab)

        self.isbn_entry.pack(fill='x', pady=2)

        

        ttk.Label(tab, text="タイトル:").pack(anchor='w', pady=(10,0))

        self.title_entry = ttk.Entry(tab)

        self.title_entry.pack(fill='x', pady=2)

        

        ttk.Label(tab, text="メモ:").pack(anchor='w', pady=(10,0))

        self.notes_text = scrolledtext.ScrolledText(tab, height=6)

        self.notes_text.pack(fill='both', expand=True, pady=2)

        

        ttk.Button(tab, text="✅ 登録してAmazon評価取得", 

                   command=self.register_book).pack(pady=10, fill='x')

        

        ttk.Button(tab, text="🗑️ クリア", 

                   command=self.clear_form).pack(fill='x')

        

        # 使い方

        help_text = """【使い方】

1. 本の裏表紙からISBNコード(13桁)を入力

2. タイトルを入力

3. 登録ボタンでAmazon評価を自動取得

4. 一覧タブで確認"""

        

        ttk.Label(tab, text=help_text, foreground='gray', 

                 justify='left').pack(pady=10, anchor='w')

    

    def create_list_tab(self):

        tab = ttk.Frame(self.notebook, padding=10)

        self.notebook.add(tab, text='📋 一覧')

        

        filter_frame = ttk.Frame(tab)

        filter_frame.pack(fill='x', pady=5)

        

        self.filter_var = tk.StringVar(value='all')

        ttk.Radiobutton(filter_frame, text="全て", variable=self.filter_var,

                       value='all', command=self.load_books).pack(side='left')

        ttk.Radiobutton(filter_frame, text="所有", variable=self.filter_var,

                       value='owned', command=self.load_books).pack(side='left')

        ttk.Radiobutton(filter_frame, text="売却済", variable=self.filter_var,

                       value='sold', command=self.load_books).pack(side='left')

        

        list_frame = ttk.Frame(tab)

        list_frame.pack(fill='both', expand=True, pady=5)

        

        scrollbar = ttk.Scrollbar(list_frame)

        scrollbar.pack(side='right', fill='y')

        

        self.book_listbox = tk.Listbox(list_frame, yscrollcommand=scrollbar.set)

        self.book_listbox.pack(side='left', fill='both', expand=True)

        scrollbar.config(command=self.book_listbox.yview)

        

        self.book_listbox.bind('<>', self.on_book_select)

        

        ttk.Label(tab, text="詳細:").pack(anchor='w')

        self.detail_text = scrolledtext.ScrolledText(tab, height=10)

        self.detail_text.pack(fill='both', expand=True, pady=5)

        

        ttk.Button(tab, text="🔄 更新", command=self.load_books).pack(fill='x')

    

    def create_sell_tab(self):

        tab = ttk.Frame(self.notebook, padding=10)

        self.notebook.add(tab, text='💰 売却')

        

        ttk.Label(tab, text="低評価本(売却候補)", 

                 font=('', 11, 'bold')).pack(pady=5)

        

        criteria_frame = ttk.Frame(tab)

        criteria_frame.pack(fill='x', pady=5)

        

        ttk.Label(criteria_frame, text="評価:").pack(side='left')

        self.rating_threshold = tk.DoubleVar(value=3.5)

        ttk.Spinbox(criteria_frame, from_=1.0, to=5.0, increment=0.5,

                   textvariable=self.rating_threshold, width=5).pack(side='left', padx=2)

        ttk.Label(criteria_frame, text="以下").pack(side='left')

        

        ttk.Button(criteria_frame, text="🔍 検索", 

                  command=self.find_low_rated).pack(side='left', padx=5, fill='x', expand=True)

        

        list_frame = ttk.Frame(tab)

        list_frame.pack(fill='both', expand=True, pady=5)

        

        scrollbar = ttk.Scrollbar(list_frame)

        scrollbar.pack(side='right', fill='y')

        

        self.sell_listbox = tk.Listbox(list_frame, yscrollcommand=scrollbar.set)

        self.sell_listbox.pack(side='left', fill='both', expand=True)

        scrollbar.config(command=self.sell_listbox.yview)

        

        price_frame = ttk.Frame(tab)

        price_frame.pack(fill='x', pady=5)

        

        ttk.Label(price_frame, text="売却価格:").pack(side='left')

        self.sell_price_entry = ttk.Entry(price_frame, width=10)

        self.sell_price_entry.pack(side='left', padx=2)

        self.sell_price_entry.insert(0, "0")

        ttk.Label(price_frame, text="円").pack(side='left')

        

        ttk.Button(tab, text="✅ 売却済みにする", 

                  command=self.mark_as_sold).pack(fill='x')

    

    def create_check_tab(self):

        tab = ttk.Frame(self.notebook, padding=10)

        self.notebook.add(tab, text='🔍 重複確認')

        

        ttk.Label(tab, text="購入前チェック", 

                 font=('', 11, 'bold')).pack(pady=5)

        

        ttk.Label(tab, text="ISBN:").pack(anchor='w')

        self.check_isbn_entry = ttk.Entry(tab)

        self.check_isbn_entry.pack(fill='x', pady=2)

        

        ttk.Label(tab, text="タイトル:").pack(anchor='w')

        self.check_title_entry = ttk.Entry(tab)

        self.check_title_entry.pack(fill='x', pady=2)

        

        ttk.Button(tab, text="🔍 重複チェック", 

                  command=self.check_duplicate).pack(pady=10, fill='x')

        

        self.check_result_text = scrolledtext.ScrolledText(tab)

        self.check_result_text.pack(fill='both', expand=True, pady=5)

    

    def create_stats_tab(self):

        tab = ttk.Frame(self.notebook, padding=10)

        self.notebook.add(tab, text='📊 統計')

        

        self.stats_text = scrolledtext.ScrolledText(tab)

        self.stats_text.pack(fill='both', expand=True, pady=5)

        

        ttk.Button(tab, text="🔄 更新", command=self.load_stats).pack(fill='x')

        

        self.load_stats()

    

    def register_book(self):

        isbn = self.isbn_entry.get().strip()

        title = self.title_entry.get().strip()

        notes = self.notes_text.get('1.0', 'end').strip()

        

        if not isbn and not title:

            messagebox.showwarning("警告", "ISBNまたはタイトルを入力してください")

            return

        

        if isbn:

            duplicates = self.db.check_duplicate(isbn=isbn)

            if duplicates:

                if not messagebox.askyesno("重複", "既に登録されています。続行しますか?"):

                    return

        

        messagebox.showinfo("処理中", "Amazon評価取得中...(15秒程度)")

        self.root.update()

        

        amazon_data = AmazonScraper.search_amazon(isbn)

        

        book_data = {

            'isbn': isbn,

            'title': title,

            'amazon_rating': amazon_data['rating'],

            'amazon_review_count': amazon_data['review_count'],

            'amazon_url': amazon_data['url'],

            'image_path': None

        }

        

        book_id = self.db.add_book(book_data)

        

        if book_id:

            rating = f"★{amazon_data['rating']}" if amazon_data['rating'] else "未評価"

            messagebox.showinfo("成功", 

                f"✅ 登録完了!\n\n{title}\nAmazon: {rating}")

            self.clear_form()

            self.load_books()

        else:

            messagebox.showerror("エラー", "登録失敗")

    

    def load_books(self):

        status = None if self.filter_var.get() == 'all' else self.filter_var.get()

        books = self.db.get_books(status=status)

        

        self.book_listbox.delete(0, 'end')

        self.current_books = books

        

        for book in books:

            rating = f"★{book['amazon_rating']}" if book['amazon_rating'] else "未評価"

            emoji = "📗" if book['status'] == 'owned' else "📕"

            self.book_listbox.insert('end', f"{emoji} {book['title'][:30]} {rating}")

    

    def on_book_select(self, event):

        sel = self.book_listbox.curselection()

        if not sel:

            return

        

        book = self.current_books[sel[0]]

        

        detail = f"""【{book['title']}】

ISBN: {book['isbn'] or '未登録'}

評価: {book['amazon_rating'] or '未取得'}

レビュー: {book['amazon_review_count'] or 0}件

ステータス: {book['status']}

購入日: {book['purchase_date'] or '不明'}

登録日: {book['created_at'][:10] if book['created_at'] else '不明'}

URL: {book['amazon_url'] or 'なし'}

"""

        

        self.detail_text.delete('1.0', 'end')

        self.detail_text.insert('1.0', detail)

    

    def find_low_rated(self):

        threshold = self.rating_threshold.get()

        books = self.db.get_low_rated_books(threshold)

        

        self.sell_listbox.delete(0, 'end')

        self.low_rated_books = books

        

        for book in books:

            self.sell_listbox.insert('end', f"{book['title'][:30]} ★{book['amazon_rating']}")

        

        messagebox.showinfo("結果", f"{len(books)}冊見つかりました")

    

    def mark_as_sold(self):

        sel = self.sell_listbox.curselection()

        if not sel:

            messagebox.showwarning("警告", "本を選択してください")

            return

        

        book = self.low_rated_books[sel[0]]

        

        try:

            price = int(self.sell_price_entry.get())

        except:

            price = 0

        

        if messagebox.askyesno("確認", f"売却済みにしますか?\n{book['title']}\n{price}円"):

            updates = {

                'status': 'sold',

                'sell_date': datetime.now().strftime('%Y-%m-%d'),

                'sell_price': price

            }

            self.db.update_book(book['id'], updates)

            messagebox.showinfo("完了", "売却済みにしました")

            self.find_low_rated()

            self.load_stats()

    

    def check_duplicate(self):

        isbn = self.check_isbn_entry.get().strip()

        title = self.check_title_entry.get().strip()

        

        if not isbn and not title:

            messagebox.showwarning("警告", "ISBNまたはタイトルを入力")

            return

        

        results = self.db.check_duplicate(isbn, title)

        

        self.check_result_text.delete('1.0', 'end')

        

        if results:

            text = f"⚠️ {len(results)}冊が既に登録されています\n\n"

            for book in results:

                rating = f"★{book['amazon_rating']}" if book['amazon_rating'] else "未評価"

                text += f"・{book['title']}\n  {rating} ({book['status']})\n\n"

            messagebox.showwarning("重複", "既に所有しています")

        else:

            text = "✅ 重複なし\n購入できます!"

            messagebox.showinfo("OK", "重複なし")

        

        self.check_result_text.insert('1.0', text)

    

    def load_stats(self):

        stats = self.db.get_stats()

        books = self.db.get_books()

        

        text = "="*35 + "\n📊 蔵書統計\n" + "="*35 + "\n\n"

        

        total = sum(s['count'] for s in stats)

        text += f"総数: {total}冊\n\n"

        

        for stat in stats:

            name = {"owned": "所有中", "sold": "売却済"}.get(stat['status'], stat['status'])

            avg = f"{stat['avg_rating']:.2f}" if stat['avg_rating'] else "N/A"

            text += f"{name}: {stat['count']}冊 (平均★{avg})\n"

        

        self.stats_text.delete('1.0', 'end')

        self.stats_text.insert('1.0', text)

    

    def clear_form(self):

        self.isbn_entry.delete(0, 'end')

        self.title_entry.delete(0, 'end')

        self.notes_text.delete('1.0', 'end')

if __name__ == '__main__':

    root = tk.Tk()

    app = BookManagerApp(root)

    root.mainloop()

コメント

このブログの人気の投稿

ミライアイ内服薬は薬事法違反で、ほとんど効果がない詐欺ですか?

最高裁での上告理由書受理・却下の判断基準について

裁判官の忌避申立書の作成例