diff --git a/docs/tasks/modpack-version-checker/code/modpack-version-checker/.gitignore b/docs/tasks/modpack-version-checker/code/modpack-version-checker/.gitignore new file mode 100644 index 0000000..ac01d92 --- /dev/null +++ b/docs/tasks/modpack-version-checker/code/modpack-version-checker/.gitignore @@ -0,0 +1 @@ +# Placeholder - content in chat history diff --git a/docs/tasks/modpack-version-checker/code/modpack-version-checker/LICENSE b/docs/tasks/modpack-version-checker/code/modpack-version-checker/LICENSE new file mode 100644 index 0000000..ac01d92 --- /dev/null +++ b/docs/tasks/modpack-version-checker/code/modpack-version-checker/LICENSE @@ -0,0 +1 @@ +# Placeholder - content in chat history diff --git a/docs/tasks/modpack-version-checker/code/modpack-version-checker/create_all_files.sh b/docs/tasks/modpack-version-checker/code/modpack-version-checker/create_all_files.sh new file mode 100755 index 0000000..210ca0f --- /dev/null +++ b/docs/tasks/modpack-version-checker/code/modpack-version-checker/create_all_files.sh @@ -0,0 +1,183 @@ +#!/bin/bash +# Auto-generated script to create all modpack-version-checker files + +BASE_DIR="/home/claude/firefrost-operations-manual/docs/tasks/modpack-version-checker/code/modpack-version-checker" +cd "$BASE_DIR" + +# Already created: src/modpack_checker/__init__.py, src/modpack_checker/config.py + +# Create database.py +cat > src/modpack_checker/database.py << 'EOF' +"""SQLite database layer using SQLAlchemy 2.0 ORM.""" + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from pathlib import Path +from typing import List, Optional + +from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, create_engine, select +from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column, relationship + + +class _Base(DeclarativeBase): + pass + + +class _ModpackRow(_Base): + __tablename__ = "modpacks" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + curseforge_id: Mapped[int] = mapped_column(Integer, unique=True, nullable=False) + name: Mapped[str] = mapped_column(String(255), nullable=False) + current_version: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) + last_checked: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) + notification_enabled: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) + + history: Mapped[List["_CheckHistoryRow"]] = relationship( + back_populates="modpack", cascade="all, delete-orphan" + ) + + +class _CheckHistoryRow(_Base): + __tablename__ = "check_history" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + modpack_id: Mapped[int] = mapped_column(ForeignKey("modpacks.id"), nullable=False) + checked_at: Mapped[datetime] = mapped_column(DateTime, nullable=False) + version_found: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) + notification_sent: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) + + modpack: Mapped["_ModpackRow"] = relationship(back_populates="history") + + +@dataclass +class Modpack: + id: int + curseforge_id: int + name: str + current_version: Optional[str] + last_checked: Optional[datetime] + notification_enabled: bool + + @classmethod + def _from_row(cls, row: _ModpackRow) -> "Modpack": + return cls( + id=row.id, + curseforge_id=row.curseforge_id, + name=row.name, + current_version=row.current_version, + last_checked=row.last_checked, + notification_enabled=row.notification_enabled, + ) + + +@dataclass +class CheckHistory: + id: int + modpack_id: int + checked_at: datetime + version_found: Optional[str] + notification_sent: bool + + @classmethod + def _from_row(cls, row: _CheckHistoryRow) -> "CheckHistory": + return cls( + id=row.id, + modpack_id=row.modpack_id, + checked_at=row.checked_at, + version_found=row.version_found, + notification_sent=row.notification_sent, + ) + + +class Database: + def __init__(self, db_path: str) -> None: + Path(db_path).parent.mkdir(parents=True, exist_ok=True) + self.engine = create_engine(f"sqlite:///{db_path}", echo=False) + _Base.metadata.create_all(self.engine) + + def add_modpack(self, curseforge_id: int, name: str) -> Modpack: + with Session(self.engine) as session: + existing = session.scalar( + select(_ModpackRow).where(_ModpackRow.curseforge_id == curseforge_id) + ) + if existing is not None: + raise ValueError(f"Modpack ID {curseforge_id} is already being tracked.") + row = _ModpackRow(curseforge_id=curseforge_id, name=name, notification_enabled=True) + session.add(row) + session.commit() + return Modpack._from_row(row) + + def remove_modpack(self, curseforge_id: int) -> bool: + with Session(self.engine) as session: + row = session.scalar( + select(_ModpackRow).where(_ModpackRow.curseforge_id == curseforge_id) + ) + if row is None: + return False + session.delete(row) + session.commit() + return True + + def update_version(self, curseforge_id: int, version: str, notification_sent: bool = False) -> None: + with Session(self.engine) as session: + row = session.scalar( + select(_ModpackRow).where(_ModpackRow.curseforge_id == curseforge_id) + ) + if row is None: + raise ValueError(f"Modpack {curseforge_id} not found in database.") + row.current_version = version + row.last_checked = datetime.utcnow() + session.add( + _CheckHistoryRow( + modpack_id=row.id, + checked_at=datetime.utcnow(), + version_found=version, + notification_sent=notification_sent, + ) + ) + session.commit() + + def toggle_notifications(self, curseforge_id: int, enabled: bool) -> bool: + with Session(self.engine) as session: + row = session.scalar( + select(_ModpackRow).where(_ModpackRow.curseforge_id == curseforge_id) + ) + if row is None: + return False + row.notification_enabled = enabled + session.commit() + return True + + def get_modpack(self, curseforge_id: int) -> Optional[Modpack]: + with Session(self.engine) as session: + row = session.scalar( + select(_ModpackRow).where(_ModpackRow.curseforge_id == curseforge_id) + ) + return Modpack._from_row(row) if row else None + + def get_all_modpacks(self) -> List[Modpack]: + with Session(self.engine) as session: + rows = session.scalars(select(_ModpackRow)).all() + return [Modpack._from_row(r) for r in rows] + + def get_check_history(self, curseforge_id: int, limit: int = 10) -> List[CheckHistory]: + with Session(self.engine) as session: + modpack_row = session.scalar( + select(_ModpackRow).where(_ModpackRow.curseforge_id == curseforge_id) + ) + if modpack_row is None: + return [] + rows = session.scalars( + select(_CheckHistoryRow) + .where(_CheckHistoryRow.modpack_id == modpack_row.id) + .order_by(_CheckHistoryRow.checked_at.desc()) + .limit(limit) + ).all() + return [CheckHistory._from_row(r) for r in rows] +EOF + +echo "Created database.py" + diff --git a/docs/tasks/modpack-version-checker/code/modpack-version-checker/requirements.txt b/docs/tasks/modpack-version-checker/code/modpack-version-checker/requirements.txt new file mode 100644 index 0000000..ac01d92 --- /dev/null +++ b/docs/tasks/modpack-version-checker/code/modpack-version-checker/requirements.txt @@ -0,0 +1 @@ +# Placeholder - content in chat history diff --git a/docs/tasks/modpack-version-checker/code/modpack-version-checker/setup.cfg b/docs/tasks/modpack-version-checker/code/modpack-version-checker/setup.cfg new file mode 100644 index 0000000..ac01d92 --- /dev/null +++ b/docs/tasks/modpack-version-checker/code/modpack-version-checker/setup.cfg @@ -0,0 +1 @@ +# Placeholder - content in chat history diff --git a/docs/tasks/modpack-version-checker/code/modpack-version-checker/setup.py b/docs/tasks/modpack-version-checker/code/modpack-version-checker/setup.py new file mode 100644 index 0000000..ac01d92 --- /dev/null +++ b/docs/tasks/modpack-version-checker/code/modpack-version-checker/setup.py @@ -0,0 +1 @@ +# Placeholder - content in chat history diff --git a/docs/tasks/modpack-version-checker/code/modpack-version-checker/src/modpack_checker/__init__.py b/docs/tasks/modpack-version-checker/code/modpack-version-checker/src/modpack_checker/__init__.py new file mode 100644 index 0000000..7905747 --- /dev/null +++ b/docs/tasks/modpack-version-checker/code/modpack-version-checker/src/modpack_checker/__init__.py @@ -0,0 +1,4 @@ +"""Modpack Version Checker - Monitor CurseForge modpack updates.""" + +__version__ = "1.0.0" +__author__ = "Firefrost Gaming" diff --git a/docs/tasks/modpack-version-checker/code/modpack-version-checker/src/modpack_checker/cli.py b/docs/tasks/modpack-version-checker/code/modpack-version-checker/src/modpack_checker/cli.py new file mode 100644 index 0000000..ac01d92 --- /dev/null +++ b/docs/tasks/modpack-version-checker/code/modpack-version-checker/src/modpack_checker/cli.py @@ -0,0 +1 @@ +# Placeholder - content in chat history diff --git a/docs/tasks/modpack-version-checker/code/modpack-version-checker/src/modpack_checker/config.py b/docs/tasks/modpack-version-checker/code/modpack-version-checker/src/modpack_checker/config.py new file mode 100644 index 0000000..2555e2b --- /dev/null +++ b/docs/tasks/modpack-version-checker/code/modpack-version-checker/src/modpack_checker/config.py @@ -0,0 +1,45 @@ +"""Configuration management for Modpack Version Checker.""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Optional + +from pydantic import BaseModel, Field + +CONFIG_DIR = Path.home() / ".config" / "modpack-checker" +CONFIG_FILE = CONFIG_DIR / "config.json" +DEFAULT_DB_PATH = str(CONFIG_DIR / "modpacks.db") + + +class Config(BaseModel): + """Application configuration, persisted to ~/.config/modpack-checker/config.json.""" + + curseforge_api_key: str = "" + discord_webhook_url: Optional[str] = None + database_path: str = DEFAULT_DB_PATH + check_interval_hours: int = Field(default=6, ge=1, le=168) + notification_on_update: bool = True + + @classmethod + def load(cls) -> "Config": + """Load configuration from disk, returning defaults if not present.""" + if CONFIG_FILE.exists(): + try: + with open(CONFIG_FILE) as f: + data = json.load(f) + return cls(**data) + except (json.JSONDecodeError, ValueError): + return cls() + return cls() + + def save(self) -> None: + """Persist configuration to disk.""" + CONFIG_DIR.mkdir(parents=True, exist_ok=True) + with open(CONFIG_FILE, "w") as f: + json.dump(self.model_dump(), f, indent=2) + + def is_configured(self) -> bool: + """Return True if the minimum required config (API key) is present.""" + return bool(self.curseforge_api_key) diff --git a/docs/tasks/modpack-version-checker/code/modpack-version-checker/src/modpack_checker/curseforge.py b/docs/tasks/modpack-version-checker/code/modpack-version-checker/src/modpack_checker/curseforge.py new file mode 100644 index 0000000..ac01d92 --- /dev/null +++ b/docs/tasks/modpack-version-checker/code/modpack-version-checker/src/modpack_checker/curseforge.py @@ -0,0 +1 @@ +# Placeholder - content in chat history diff --git a/docs/tasks/modpack-version-checker/code/modpack-version-checker/src/modpack_checker/database.py b/docs/tasks/modpack-version-checker/code/modpack-version-checker/src/modpack_checker/database.py new file mode 100644 index 0000000..ac01d92 --- /dev/null +++ b/docs/tasks/modpack-version-checker/code/modpack-version-checker/src/modpack_checker/database.py @@ -0,0 +1 @@ +# Placeholder - content in chat history diff --git a/docs/tasks/modpack-version-checker/code/modpack-version-checker/src/modpack_checker/notifier.py b/docs/tasks/modpack-version-checker/code/modpack-version-checker/src/modpack_checker/notifier.py new file mode 100644 index 0000000..ac01d92 --- /dev/null +++ b/docs/tasks/modpack-version-checker/code/modpack-version-checker/src/modpack_checker/notifier.py @@ -0,0 +1 @@ +# Placeholder - content in chat history diff --git a/docs/tasks/modpack-version-checker/code/modpack-version-checker/tests/conftest.py b/docs/tasks/modpack-version-checker/code/modpack-version-checker/tests/conftest.py new file mode 100644 index 0000000..ac01d92 --- /dev/null +++ b/docs/tasks/modpack-version-checker/code/modpack-version-checker/tests/conftest.py @@ -0,0 +1 @@ +# Placeholder - content in chat history diff --git a/docs/tasks/modpack-version-checker/code/modpack-version-checker/tests/test_config.py b/docs/tasks/modpack-version-checker/code/modpack-version-checker/tests/test_config.py new file mode 100644 index 0000000..ac01d92 --- /dev/null +++ b/docs/tasks/modpack-version-checker/code/modpack-version-checker/tests/test_config.py @@ -0,0 +1 @@ +# Placeholder - content in chat history diff --git a/docs/tasks/modpack-version-checker/code/modpack-version-checker/tests/test_curseforge.py b/docs/tasks/modpack-version-checker/code/modpack-version-checker/tests/test_curseforge.py new file mode 100644 index 0000000..ac01d92 --- /dev/null +++ b/docs/tasks/modpack-version-checker/code/modpack-version-checker/tests/test_curseforge.py @@ -0,0 +1 @@ +# Placeholder - content in chat history diff --git a/docs/tasks/modpack-version-checker/code/modpack-version-checker/tests/test_database.py b/docs/tasks/modpack-version-checker/code/modpack-version-checker/tests/test_database.py new file mode 100644 index 0000000..ac01d92 --- /dev/null +++ b/docs/tasks/modpack-version-checker/code/modpack-version-checker/tests/test_database.py @@ -0,0 +1 @@ +# Placeholder - content in chat history diff --git a/docs/tasks/modpack-version-checker/code/modpack-version-checker/tests/test_notifier.py b/docs/tasks/modpack-version-checker/code/modpack-version-checker/tests/test_notifier.py new file mode 100644 index 0000000..ac01d92 --- /dev/null +++ b/docs/tasks/modpack-version-checker/code/modpack-version-checker/tests/test_notifier.py @@ -0,0 +1 @@ +# Placeholder - content in chat history