File size: 6,746 Bytes
a96145c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48aff15
 
 
 
 
 
 
 
f02f359
27b6909
 
f02f359
 
 
 
27b6909
f02f359
27b6909
 
 
 
f02f359
 
 
a96145c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
623f05d
 
a96145c
 
 
 
 
 
48aff15
f02f359
 
 
 
 
48aff15
 
f02f359
 
 
 
 
 
 
 
 
a96145c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48aff15
a96145c
48aff15
 
 
 
 
 
a96145c
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
#!/usr/bin/env python3

import argparse
import logging
import sys
import time

from config import Config
from src.models import Article, NewsItem
from src.extractor import ArticleExtractor
from src.analyzer import NewsAnalyzer
from src.aggregator import NewsAggregator
from src.presenter import NewsPresenter

logger = logging.getLogger(__name__)

TRANSLATE_DOMAINS = {
    "nawaat.org", "www.nawaat.org",
    "tunisienumerique.com", "www.tunisienumerique.com",
    "lapresse.tn", "www.lapresse.tn",
    "webmanagercenter.com", "www.webmanagercenter.com",
    "directinfo.webmanagercenter.com",
    "tuniscope.com", "www.tuniscope.com",
}

_translator = None


def _translate_text(text: str, target: str = "en") -> str:
    if not text or len(text.strip()) < 3:
        return text
    global _translator
    try:
        if _translator is None:
            from googletrans import Translator
            _translator = Translator()
        return _translator.translate(text[:2000], dest=target).text
    except Exception:
        return text


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="OneNews β€” RSS news aggregator")
    parser.add_argument("--demo", action="store_true", help="Use sample data (no internet)")
    parser.add_argument("--source", choices=["feeds", "hn", "reddit"], default=None,
                        help="Data source (default: RSS feeds)")
    parser.add_argument("--models", action="store_true", help="Enable local ML models (slower)")
    parser.add_argument("--subreddits", nargs="+", default=None, help="Override subreddits")
    parser.add_argument("--limit", type=int, default=None, help="Posts per subreddit")
    return parser.parse_args()


def run_pipeline(items: list[NewsItem], cfg: Config, skip_extraction: bool = False):
    if not skip_extraction:
        logger.info("═══  Extracting article content  ═══")
        extractor = ArticleExtractor()
        for i, item in enumerate(items, 1):
            article = extractor.extract(item.post.url)
            if article:
                logger.info("  [%2d/%d] %-60s βœ“  (%s)", i, len(items), item.post.title[:60], article.source_domain)
            else:
                article = Article(
                    url=item.post.url,
                    title=item.post.title,
                    text=item.post.title,
                    source_domain=item.post.source_domain or "reddit.com",
                    extraction_success=False,
                    image_url=item.post.image_url,
                    published=item.post.published,
                    published_iso=item.post.published_iso,
                )
                logger.info("  [%2d/%d] %-60s βœ—  (title only)", i, len(items), item.post.title[:60])
            item.article = article
    else:
        logger.info("═══  Extraction skipped (articles already loaded)  ═══")

    logger.info("═══  Translating Arab/Tunisian articles  ═══")
    for item in items:
        art = item.article
        post = item.post
        if not art or not post or not post.source_domain:
            continue
        domain = post.source_domain.lower()
        if domain not in TRANSLATE_DOMAINS:
            continue
        t_title = _translate_text(art.title)
        if t_title and t_title != art.title:
            logger.info("  Title: %s β†’ %s", art.title[:50], t_title[:50])
            art.title = t_title
        t_text = _translate_text(art.text)
        if t_text and t_text != art.text:
            art.text = t_text

    logger.info("═══  Analysing articles  ═══")
    analyzer = NewsAnalyzer(cfg)
    for i, item in enumerate(items, 1):
        item.analysis = analyzer.analyze(item.article)
        cat = item.analysis.category
        topics = ", ".join(item.analysis.topics) if item.analysis.topics else "(none)"
        logger.info("  [%2d/%d] %-14s topic: %-30s trust: %s", i, len(items), cat, topics, f"{item.analysis.trustworthiness_score:.0%}")

    logger.info("═══  Clustering & ranking  ═══")
    aggregator = NewsAggregator(cfg)
    clusters = aggregator.cluster_news(items)
    logger.info("  β†’ %d story clusters found", len(clusters))

    return clusters


def main():
    logging.basicConfig(level=logging.INFO, format="%(levelname).1s %(message)s", stream=sys.stderr)
    args = parse_args()
    cfg = Config()

    if args.models:
        cfg.use_local_models = True
        logging.getLogger().setLevel(logging.DEBUG)
    if args.subreddits:
        cfg.news_subreddits = args.subreddits
    if args.limit:
        cfg.posts_per_subreddit = args.limit

    total_start = time.perf_counter()

    source = args.source or "feeds"

    if args.demo:
        from src.mockdata import generate_demo_items
        print("═══  Loading demo data  ═══")
        items = generate_demo_items()
        print(f"  β†’ {len(items)} sample articles loaded\n")
        clusters = run_pipeline(items, cfg, skip_extraction=True)
        n_posts = len(items)

    elif source == "hn":
        from src.hn_scraper import HackerNewsScraper
        print("═══  Scraping Hacker News  ═══")
        scraper = HackerNewsScraper(cfg)
        posts = scraper.fetch_posts()
        n_posts = len(posts)
        print(f"  β†’ {n_posts} posts collected\n")
        if not posts:
            sys.exit(1)
        items = [NewsItem(post=p) for p in posts]
        clusters = run_pipeline(items, cfg)

    elif source == "reddit":
        from src.scraper import RedditScraper
        print("═══  Scraping Reddit  ═══")
        scraper = RedditScraper(cfg)
        posts = scraper.fetch_posts()
        n_posts = len(posts)
        print(f"  β†’ {n_posts} posts collected\n")
        if not posts:
            sys.exit(1)
        items = [NewsItem(post=p) for p in posts]
        clusters = run_pipeline(items, cfg)

    else:
        from src.rss_feed_scraper import RSSFeedScraper
        from src.html_scraper import HTMLSiteScraper
        print("═══  Fetching RSS news feeds  ═══")
        rss = RSSFeedScraper(cfg).fetch_posts()
        print(f"  β†’ {len(rss)} RSS posts collected")
        print("═══  Scraping HTML sites  ═══")
        html_posts = HTMLSiteScraper(cfg).fetch_posts()
        print(f"  β†’ {len(html_posts)} HTML posts collected")
        posts = rss + html_posts
        n_posts = len(posts)
        if not posts:
            sys.exit(1)
        items = [NewsItem(post=p) for p in posts]
        clusters = run_pipeline(items, cfg)

    elapsed = time.perf_counter() - total_start
    print(f"\n  Done in {elapsed:.1f}s β€” {n_posts} posts, {len(clusters)} clusters\n")


if __name__ == "__main__":
    main()