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()