News Headlines Fetcher

import json

import os

import textwrap

import webbrowser

from datetime import datetime, timedelta

from pathlib import Path


try:

    import requests

    REQUESTS_OK = True

except ImportError:

    REQUESTS_OK = False


# ============================================================

# CONFIGURATION

# ============================================================


# Get your FREE API key at: https://newsapi.org/register

# Free tier: 100 requests/day, last 30 days of news

API_KEY_FILE  = "newsapi_key.txt"

CACHE_FILE    = "news_cache.json"

SAVED_FILE    = "saved_articles.json"

HISTORY_FILE  = "news_history.json"

CACHE_EXPIRY  = 900      # seconds (15 minutes)


BASE_URL      = "https://newsapi.org/v2/"


CATEGORIES = [

    "general", "business", "technology",

    "science", "health", "sports", "entertainment"

]


COUNTRIES = {

    "us": "United States",

    "in": "India",

    "gb": "United Kingdom",

    "au": "Australia",

    "ca": "Canada",

    "de": "Germany",

    "fr": "France",

    "jp": "Japan",

    "sg": "Singapore",

    "ae": "UAE",

}


# ============================================================

# API KEY MANAGEMENT

# ============================================================


def load_api_key():

    if Path(API_KEY_FILE).exists():

        key = Path(API_KEY_FILE).read_text().strip()

        if key:

            return key

    return None



def save_api_key(key):

    Path(API_KEY_FILE).write_text(key.strip())

    print(f"  API key saved to {API_KEY_FILE}")



def setup_api_key():

    print("\n" + "="*55)

    print("  NEWSAPI KEY SETUP")

    print("="*55)

    print("\n  This program uses NewsAPI (newsapi.org).")

    print("  Free tier: 100 requests/day | No credit card needed.\n")

    print("  Steps:")

    print("  1. Go to: https://newsapi.org/register")

    print("  2. Sign up for free")

    print("  3. Copy your API key")

    print("  4. Paste it below\n")


    key = input("  Paste your API key: ").strip()

    if key:

        save_api_key(key)

        return key

    return None



# ============================================================

# CACHE MANAGEMENT

# ============================================================


def load_cache():

    if Path(CACHE_FILE).exists():

        try:

            with open(CACHE_FILE, "r", encoding="utf-8") as f:

                return json.load(f)

        except:

            pass

    return {}



def save_cache(cache):

    with open(CACHE_FILE, "w", encoding="utf-8") as f:

        json.dump(cache, f, indent=2, ensure_ascii=False)



def get_cache_key(endpoint, params):

    key_parts = [endpoint] + [f"{k}={v}" for k, v in sorted(params.items())]

    return "|".join(key_parts)



def is_cache_valid(cache, key):

    if key not in cache:

        return False

    age = datetime.now().timestamp() - cache[key].get("timestamp", 0)

    return age < CACHE_EXPIRY



# ============================================================

# API REQUESTS

# ============================================================


def api_request(endpoint, params, api_key):

    """

    Make a NewsAPI request with caching.

    Returns articles list or None on failure.

    """

    cache     = load_cache()

    cache_key = get_cache_key(endpoint, params)


    # Return cached if fresh

    if is_cache_valid(cache, cache_key):

        age = int(datetime.now().timestamp() - cache[cache_key]["timestamp"])

        print(f"  Using cached results (age: {age}s)")

        return cache[cache_key]["articles"]


    if not REQUESTS_OK:

        print("  requests not installed.")

        return None


    params["apiKey"] = api_key

    url = BASE_URL + endpoint


    try:

        print("  Fetching from NewsAPI...")

        resp = requests.get(url, params=params, timeout=12)


        if resp.status_code == 401:

            print("  Invalid API key. Please check your key (Option 7).")

            return None

        if resp.status_code == 429:

            print("  Rate limit reached. Please wait or upgrade your plan.")

            return None

        resp.raise_for_status()


        data     = resp.json()

        articles = data.get("articles", [])


        # Save to cache

        cache[cache_key] = {

            "timestamp": datetime.now().timestamp(),

            "articles":  articles,

            "total":     data.get("totalResults", 0),

        }

        save_cache(cache)


        print(f"  Fetched {len(articles)} article(s)")

        return articles


    except requests.exceptions.ConnectionError:

        print("  No internet connection.")

        # Try returning stale cache

        if cache_key in cache:

            print("  Using stale cached results as fallback.")

            return cache[cache_key]["articles"]

        return None

    except Exception as e:

        print(f"  API error: {e}")

        return None



# ============================================================

# FETCH TOP HEADLINES

# ============================================================


def fetch_top_headlines(api_key, category="general",

                        country="us", page_size=10):

    params = {

        "category": category,

        "country":  country,

        "pageSize": page_size,

    }

    return api_request("top-headlines", params, api_key)



# ============================================================

# FETCH BY KEYWORD

# ============================================================


def fetch_by_keyword(api_key, keyword, from_days=1,

                     sort_by="publishedAt", page_size=10,

                     language="en"):

    from_date = (datetime.now() - timedelta(days=from_days)

                 ).strftime("%Y-%m-%d")

    params = {

        "q":        keyword,

        "from":     from_date,

        "sortBy":   sort_by,

        "language": language,

        "pageSize": page_size,

    }

    return api_request("everything", params, api_key)



# ============================================================

# FETCH BY SOURCE

# ============================================================


def fetch_by_source(api_key, source_id, page_size=10):

    params = {

        "sources":  source_id,

        "pageSize": page_size,

    }

    return api_request("top-headlines", params, api_key)



# ============================================================

# FORMAT & DISPLAY

# ============================================================


def format_date(date_str):

    if not date_str:

        return ""

    try:

        dt = datetime.strptime(date_str[:19], "%Y-%m-%dT%H:%M:%S")

        return dt.strftime("%d %b %Y  %H:%M")

    except:

        return date_str[:10]



def display_headlines(articles, title="TOP HEADLINES"):

    if not articles:

        print("\n  No articles found.")

        return


    print("\n" + "="*65)

    print(f"  {title}  ({len(articles)} articles)")

    print("="*65)


    for i, art in enumerate(articles, 1):

        source    = art.get("source", {}).get("name", "Unknown")

        headline  = art.get("title", "No title") or "No title"

        pub_date  = format_date(art.get("publishedAt", ""))

        author    = art.get("author", "") or ""


        # Clean up "[Removed]" articles

        if "[Removed]" in headline:

            continue


        print(f"\n  [{i:02d}] {headline[:70]}")

        print(f"        {source:<25} {pub_date}")

        if author and author != source:

            print(f"        By: {author[:50]}")


    print("\n" + "="*65)



def display_article_detail(article):

    """Show full details of one article."""

    w = 65

    print("\n" + "="*w)

    headline = article.get("title", "No title") or "No title"

    print(f"  {headline}")

    print("="*w)


    source   = article.get("source", {}).get("name", "")

    author   = article.get("author", "") or ""

    pub_date = format_date(article.get("publishedAt", ""))

    desc     = article.get("description", "") or ""

    content  = article.get("content", "") or ""

    url      = article.get("url", "") or ""


    if source:

        print(f"  Source  : {source}")

    if author and author != source:

        print(f"  Author  : {author[:60]}")

    if pub_date:

        print(f"  Date    : {pub_date}")

    print("-"*w)


    if desc:

        print("\n  DESCRIPTION:")

        print(textwrap.fill(desc, width=62,

                            initial_indent="  ",

                            subsequent_indent="  "))


    if content:

        # NewsAPI truncates content at 200 chars

        content_clean = content.split("[+")[0].strip()

        if content_clean:

            print("\n  CONTENT PREVIEW:")

            print(textwrap.fill(content_clean, width=62,

                                initial_indent="  ",

                                subsequent_indent="  "))


    if url:

        print(f"\n  Full article: {url}")


    print("="*w)



# ============================================================

# SAVED ARTICLES

# ============================================================


def save_article(article):

    saved = load_saved_articles()

    url   = article.get("url", "")

    if any(s.get("url") == url for s in saved):

        print("  Already saved.")

        return

    article["saved_at"] = datetime.now().strftime("%d-%m-%Y %H:%M")

    saved.append(article)

    with open(SAVED_FILE, "w", encoding="utf-8") as f:

        json.dump(saved, f, indent=2, ensure_ascii=False)

    print(f"  Saved: {article.get('title','')[:50]}")



def load_saved_articles():

    if Path(SAVED_FILE).exists():

        try:

            with open(SAVED_FILE, "r", encoding="utf-8") as f:

                return json.load(f)

        except:

            pass

    return []



def view_saved_articles():

    saved = load_saved_articles()

    if not saved:

        print("\n  No saved articles yet.")

        return


    display_headlines(saved, "SAVED ARTICLES")


    choice = input("\n  View details? (number/Enter to back): ").strip()

    if choice.isdigit():

        idx = int(choice) - 1

        if 0 <= idx < len(saved):

            display_article_detail(saved[idx])

            open_browser = input("\n  Open in browser? (y/n): ").strip().lower()

            if open_browser == "y" and saved[idx].get("url"):

                webbrowser.open(saved[idx]["url"])



def remove_saved_article():

    saved = load_saved_articles()

    if not saved:

        print("\n  No saved articles.")

        return

    display_headlines(saved, "SAVED ARTICLES")

    try:

        idx = int(input("\n  Enter number to remove: ").strip()) - 1

        if 0 <= idx < len(saved):

            removed = saved.pop(idx)

            with open(SAVED_FILE, "w", encoding="utf-8") as f:

                json.dump(saved, f, indent=2, ensure_ascii=False)

            print(f"  Removed: {removed.get('title','')[:50]}")

        else:

            print("  Invalid number.")

    except ValueError:

        print("  Invalid input.")



# ============================================================

# SEARCH HISTORY LOG

# ============================================================


def log_search(query_type, query_value):

    history = []

    if Path(HISTORY_FILE).exists():

        try:

            with open(HISTORY_FILE, "r") as f:

                history = json.load(f)

        except:

            pass

    history.append({

        "type":      query_type,

        "query":     query_value,

        "timestamp": datetime.now().strftime("%d-%m-%Y %H:%M:%S"),

    })

    history = history[-30:]

    with open(HISTORY_FILE, "w") as f:

        json.dump(history, f, indent=2)



def view_history():

    if not Path(HISTORY_FILE).exists():

        print("\n  No search history yet.")

        return

    try:

        with open(HISTORY_FILE, "r") as f:

            history = json.load(f)

    except:

        print("  Could not load history.")

        return


    print("\n" + "="*55)

    print(f"  SEARCH HISTORY  ({len(history)} entries)")

    print("="*55)

    for h in reversed(history[-15:]):

        print(f"  {h['timestamp']}  [{h['type']}]  {h['query']}")

    print("="*55)



# ============================================================

# INTERACTIVE ARTICLE VIEWER

# ============================================================


def browse_articles(articles, title="HEADLINES"):

    """Let user browse, view details, save, open articles."""

    if not articles:

        return


    display_headlines(articles, title)


    while True:

        print("\n  Options:")

        print("  [number] = View article details")

        print("  s[num]   = Save article (e.g. s3)")

        print("  o[num]   = Open in browser (e.g. o2)")

        print("  b        = Back to menu")


        cmd = input("\n  > ").strip().lower()


        if cmd == "b" or cmd == "":

            break


        elif cmd.startswith("s") and cmd[1:].isdigit():

            idx = int(cmd[1:]) - 1

            if 0 <= idx < len(articles):

                save_article(articles[idx])

            else:

                print("  Invalid number.")


        elif cmd.startswith("o") and cmd[1:].isdigit():

            idx = int(cmd[1:]) - 1

            if 0 <= idx < len(articles):

                url = articles[idx].get("url", "")

                if url:

                    webbrowser.open(url)

                    print(f"  Opened in browser: {url[:60]}")

                else:

                    print("  No URL available.")

            else:

                print("  Invalid number.")


        elif cmd.isdigit():

            idx = int(cmd) - 1

            if 0 <= idx < len(articles):

                display_article_detail(articles[idx])

                sub = input("\n  Save? (s) | Open browser? (o) | Back (Enter): ").strip().lower()

                if sub == "s":

                    save_article(articles[idx])

                elif sub == "o":

                    url = articles[idx].get("url", "")

                    if url:

                        webbrowser.open(url)

            else:

                print("  Invalid number.")


        else:

            print("  Unknown command.")



# ============================================================

# POPULAR SOURCES

# ============================================================


POPULAR_SOURCES = {

    "1":  ("bbc-news",          "BBC News"),

    "2":  ("reuters",           "Reuters"),

    "3":  ("the-hindu",         "The Hindu"),

    "4":  ("the-times-of-india","Times of India"),

    "5":  ("techcrunch",        "TechCrunch"),

    "6":  ("wired",             "Wired"),

    "7":  ("the-verge",         "The Verge"),

    "8":  ("espn",              "ESPN"),

    "9":  ("national-geographic","National Geographic"),

    "10": ("bloomberg",         "Bloomberg"),

}



def fetch_from_source(api_key):

    print("\n  POPULAR SOURCES:")

    print("  " + "-"*35)

    for num, (src_id, name) in POPULAR_SOURCES.items():

        print(f"  {num:>3}. {name}")

    print("  " + "-"*35)


    choice = input("\n  Select source (number): ").strip()

    if choice not in POPULAR_SOURCES:

        print("  Invalid choice.")

        return


    src_id, name = POPULAR_SOURCES[choice]

    n = input(f"  Number of articles (default 10): ").strip()

    n = int(n) if n.isdigit() else 10


    articles = fetch_by_source(api_key, src_id, n)

    if articles:

        log_search("source", name)

        browse_articles(articles, f"FROM: {name.upper()}")



# ============================================================

# MAIN MENU

# ============================================================


def print_menu(api_key, country, category):

    saved_count = len(load_saved_articles())

    print("\n" + "-"*52)

    print(f"  NEWS HEADLINES FETCHER")

    print(f"  Country: {COUNTRIES.get(country, country)}  "

          f"| Category: {category.capitalize()}")

    print("-"*52)

    print("  1.  Top headlines (current settings)")

    print("  2.  Headlines by category")

    print("  3.  Search by keyword")

    print("  4.  Headlines by country")

    print("  5.  News from specific source")

    print("  6.  Trending (multiple categories)")

    print(f"  7.  Saved articles  [{saved_count}]")

    print("  8.  Search history")

    print("  9.  Change API key")

    print("  0.  Exit")

    print("-"*52)



def main():

    print("\n" + "="*55)

    print("     NEWS HEADLINES FETCHER")

    print("="*55)


    if not REQUESTS_OK:

        print("\n  requests not installed!")

        print("  Install it:  pip install requests")

        return


    # Load or setup API key

    api_key = load_api_key()

    if not api_key:

        print("\n  No API key found.")

        api_key = setup_api_key()

        if not api_key:

            print("  Cannot continue without API key.")

            return


    print(f"\n  API key loaded.")

    print("  Free tier: 100 requests/day from newsapi.org\n")


    current_country  = "us"

    current_category = "general"


    while True:

        print_menu(api_key, current_country, current_category)

        choice = input("  > ").strip()


        # ── Top Headlines ────────────────────────────────────

        if choice == "1":

            n = input("  Number of articles (default 10): ").strip()

            n = int(n) if n.isdigit() else 10

            articles = fetch_top_headlines(

                api_key, current_category, current_country, n

            )

            if articles:

                log_search("top-headlines",

                           f"{current_category}/{current_country}")

                browse_articles(

                    articles,

                    f"TOP {current_category.upper()} — "

                    f"{COUNTRIES.get(current_country, current_country).upper()}"

                )


        # ── By Category ──────────────────────────────────────

        elif choice == "2":

            print("\n  Categories:")

            for i, cat in enumerate(CATEGORIES, 1):

                marker = " ← current" if cat == current_category else ""

                print(f"  {i}. {cat.capitalize()}{marker}")

            cat_choice = input("\n  Select (number): ").strip()

            if cat_choice.isdigit():

                idx = int(cat_choice) - 1

                if 0 <= idx < len(CATEGORIES):

                    current_category = CATEGORIES[idx]

                    n = input("  Number of articles (default 10): ").strip()

                    n = int(n) if n.isdigit() else 10

                    articles = fetch_top_headlines(

                        api_key, current_category, current_country, n

                    )

                    if articles:

                        log_search("category", current_category)

                        browse_articles(

                            articles,

                            f"{current_category.upper()} NEWS"

                        )


        # ── Keyword Search ───────────────────────────────────

        elif choice == "3":

            keyword = input("\n  Search keyword: ").strip()

            if not keyword:

                continue

            days = input("  Search last N days (default 1): ").strip()

            days = int(days) if days.isdigit() else 1

            n    = input("  Number of articles (default 10): ").strip()

            n    = int(n) if n.isdigit() else 10


            print("\n  Sort by: 1=Latest  2=Relevancy  3=Popularity")

            sort_opt = input("  Sort (default 1): ").strip()

            sort_map = {"1": "publishedAt", "2": "relevancy",

                        "3": "popularity"}

            sort_by  = sort_map.get(sort_opt, "publishedAt")


            articles = fetch_by_keyword(api_key, keyword, days,

                                        sort_by, n)

            if articles:

                log_search("keyword", keyword)

                browse_articles(articles,

                                f"NEWS: '{keyword.upper()}'")


        # ── By Country ───────────────────────────────────────

        elif choice == "4":

            print("\n  Available countries:")

            for i, (code, name) in enumerate(COUNTRIES.items(), 1):

                marker = " ← current" if code == current_country else ""

                print(f"  {i:>3}. {name} ({code}){marker}")

            country_choice = input("\n  Select (number): ").strip()

            if country_choice.isdigit():

                idx = int(country_choice) - 1

                codes = list(COUNTRIES.keys())

                if 0 <= idx < len(codes):

                    current_country = codes[idx]

                    articles = fetch_top_headlines(

                        api_key, current_category, current_country

                    )

                    if articles:

                        log_search("country", current_country)

                        browse_articles(

                            articles,

                            f"TOP NEWS — "

                            f"{COUNTRIES[current_country].upper()}"

                        )


        # ── By Source ────────────────────────────────────────

        elif choice == "5":

            fetch_from_source(api_key)


        # ── Trending (Multi-category) ────────────────────────

        elif choice == "6":

            print("\n  Fetching trending headlines across categories...")

            all_articles = []

            for cat in ["technology", "business", "science", "health"]:

                arts = fetch_top_headlines(api_key, cat, current_country, 3)

                if arts:

                    for a in arts:

                        a["_category"] = cat

                    all_articles.extend(arts)

            if all_articles:

                log_search("trending", "multi-category")

                browse_articles(all_articles, "TRENDING ACROSS CATEGORIES")


        # ── Saved Articles ───────────────────────────────────

        elif choice == "7":

            print("\n  1. View saved  2. Remove article")

            sub = input("  Choice: ").strip()

            if sub == "1":

                view_saved_articles()

            elif sub == "2":

                remove_saved_article()


        # ── History ──────────────────────────────────────────

        elif choice == "8":

            view_history()


        # ── Change API Key ───────────────────────────────────

        elif choice == "9":

            new_key = input("\n  Enter new API key: ").strip()

            if new_key:

                save_api_key(new_key)

                api_key = new_key


        elif choice == "0":

            print("\n  Goodbye! Stay informed!\n")

            break


        else:

            print("  Invalid choice.")



# ============================================================

# RUN

# ============================================================


if __name__ == "__main__":

    main()

No comments: