""" Data loading layer. What changed from the original CSV-only version: - load_data() now initialises MySQL, runs the one-time CSV migration if needed, then loads the films DataFrame from MySQL (not directly from the CSV). - Reviews are no longer loaded into memory at startup. get_reviews_df() returns None (kept for compatibility). Use get_reviews_for_film(rt_link) to query per-film from MySQL instead. - reload_films() lets the daily updater refresh the in-memory DataFrame after inserting new TMDB films, without restarting the app. """ import pandas as pd import os import logging import db import migrate as migration logger = logging.getLogger(__name__) DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data") _RT_DATASET = "stefanoleone992/rotten-tomatoes-movies-and-critic-reviews-dataset" _IMDB_DATASET = "lakshmi25npathi/imdb-dataset-of-50k-movie-reviews" _REQUIRED_FILES = { "rotten_tomatoes_movies.csv": _RT_DATASET, "rotten_tomatoes_critic_reviews.csv": _RT_DATASET, "IMDB Dataset.csv": _IMDB_DATASET, } # in-memory films DataFrame - 17k rows, fast for search/leaderboard/overview # reviews are NOT loaded here; they come from MySQL per-film on demand _movies_df = None # Kaggle download def _kaggle_download(dataset_slug: str, dest_dir: str): try: import kaggle except ImportError: raise RuntimeError("kaggle package not installed. Run: pip install kaggle") logger.info(f"Downloading {dataset_slug} from Kaggle...") kaggle.api.authenticate() kaggle.api.dataset_download_files(dataset_slug, path=dest_dir, unzip=True, quiet=False) logger.info(f"Done: {dataset_slug}") def ensure_data(): os.makedirs(DATA_DIR, exist_ok=True) missing_by_dataset: dict[str, list[str]] = {} for fname, slug in _REQUIRED_FILES.items(): if not os.path.exists(os.path.join(DATA_DIR, fname)): missing_by_dataset.setdefault(slug, []).append(fname) if not missing_by_dataset: logger.info("All data files present - skipping Kaggle download") return for slug, files in missing_by_dataset.items(): logger.info(f"Missing {files} - pulling {slug} from Kaggle") _kaggle_download(slug, DATA_DIR) for fname in _REQUIRED_FILES: fpath = os.path.join(DATA_DIR, fname) if not os.path.exists(fpath): raise FileNotFoundError( f"{fname} still missing after Kaggle download. " f"Check your credentials and that the dataset name hasn't changed." ) # startup sequence def load_data(): db.init_db() if db.is_empty(): ensure_data() else: logger.info("Database already populated - skipping CSV download") migration.run_migration() _reload_films_from_db() count = len(_movies_df) if _movies_df is not None else 0 logger.info(f"Loaded {count} films into memory from MySQL") def _reload_films_from_db(): #Rebuild the in-memory DataFrame from the MySQL films table global _movies_df records = db.get_all_films_as_dicts() if not records: _movies_df = pd.DataFrame() return _movies_df = pd.DataFrame(records) # MySQL returns strings for some numeric columns depending on the driver version for col in ("tomatometer_rating", "audience_rating", "divergence_score"): if col in _movies_df.columns: _movies_df[col] = pd.to_numeric(_movies_df[col], errors="coerce") if "release_year" in _movies_df.columns: _movies_df["release_year"] = pd.to_numeric(_movies_df["release_year"], errors="coerce") if "review_fetch_active" in _movies_df.columns: _movies_df["review_fetch_active"] = pd.to_numeric( _movies_df["review_fetch_active"], errors="coerce" ) def reload_films(): #Refresh the in-memory DataFrame after the daily updater adds new films. #Called by APScheduler after run_daily_update() completes. _reload_films_from_db() count = len(_movies_df) if _movies_df is not None else 0 logger.info(f"Films reloaded - {count} total in memory") # public accessors def get_movies_df() -> pd.DataFrame | None: return _movies_df def get_reviews_df(): #Kept for backward compatibility - now always returns None. #All review queries go through get_reviews_for_film() instead. return None def get_reviews_for_film(rt_link: str) -> list: #Returns all reviews for a film directly from MySQL. #Each dict includes pre-computed IMDB sentiment columns #(sentiment_fast_label etc.) which may be None if not yet cached. return db.get_reviews_for_film(rt_link) def get_merged_df() -> pd.DataFrame | None: # kept for backward compatibility - same as get_movies_df() return _movies_df def is_loaded() -> bool: return _movies_df is not None