feat(zopk): Skrypt do naprawy źródeł newsów z Google News

Problem: Newsy z Google News RSS miały source_domain='news.google.com'
i favicon Google zamiast prawdziwego źródła.

Rozwiązanie: Nowy skrypt fix_google_news_sources.py który:
- Wyciąga nazwę źródła z tytułu (po " - ")
- Mapuje 59 źródeł na ich prawdziwe domeny
- Aktualizuje source_domain i image_url (favicon)

Wynik: 143/143 newsów zaktualizowanych z poprawnymi źródłami.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-01-15 08:06:40 +01:00
parent 8055589a08
commit c13ad09e3a

View File

@ -0,0 +1,220 @@
#!/usr/bin/env python3
"""
Skrypt do naprawy źródeł newsów z Google News.
Problem: Newsy z Google News RSS mają source_domain='news.google.com'
i favicon Google zamiast prawdziwego źródła.
Rozwiązanie: Wyciągnij nazwę źródła z tytułu (po " - ") i zaktualizuj:
- source_domain na prawdziwą domenę
- image_url na favicon prawdziwej domeny
Użycie:
python scripts/fix_google_news_sources.py --dry-run # Test
python scripts/fix_google_news_sources.py # Produkcja
"""
import os
import sys
import argparse
# Dodaj ścieżkę projektu
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, PROJECT_ROOT)
from dotenv import load_dotenv
load_dotenv(os.path.join(PROJECT_ROOT, '.env'))
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
DATABASE_URL = os.getenv('DATABASE_URL')
if not DATABASE_URL:
print("❌ Błąd: Brak zmiennej DATABASE_URL w .env")
sys.exit(1)
# Mapowanie nazw źródeł na domeny
# Klucz: nazwa źródła z tytułu (po " - ")
# Wartość: domena do użycia w favicon URL
SOURCE_TO_DOMAIN = {
# Portale z .pl w nazwie - użyj bezpośrednio
"Bankier.pl": "bankier.pl",
"Bizblog.pl": "bizblog.pl",
"Bydgoszcz.Wyborcza.pl": "bydgoszcz.wyborcza.pl",
"CIRE.pl": "cire.pl",
"GazetaPrawna.pl": "gazetaprawna.pl",
"GospodarkaMorska.pl": "gospodarkamorska.pl",
"Gov.pl": "gov.pl",
"Gramwzielone.pl": "gramwzielone.pl",
"Green-news.pl": "green-news.pl",
"Inzynieria.com": "inzynieria.com",
"Money.pl": "money.pl",
"PolsatNews.pl": "polsatnews.pl",
"Trojmiasto.pl": "trojmiasto.pl",
"ekoszalin.pl": "ekoszalin.pl",
"enerad.pl": "enerad.pl",
"naTemat.pl": "natemat.pl",
"polskieradio.pl": "polskieradio.pl",
"trojmiasto.wyborcza.pl": "trojmiasto.wyborcza.pl",
"wnp.pl": "wnp.pl",
"www.wejherowo.pl": "wejherowo.pl",
"xyz.pl": "xyz.pl",
# Portale biznesowe
"Biznes Interia": "biznes.interia.pl",
"Business Insider Polska": "businessinsider.com.pl",
"Forbes": "forbes.pl",
"Forsal": "forsal.pl",
"Newsweek": "newsweek.pl",
"Obserwator Finansowy": "obserwatorfinansowy.pl",
"Rzeczpospolita": "rp.pl",
"wGospodarce": "wgospodarce.pl",
"Strefa Biznesu": "strefabiznesu.pl",
# Portale branżowe
"Defence24": "defence24.pl",
"Energetyka24": "energetyka24.com",
"GlobEnergia": "globenergia.pl",
"Investmap": "investmap.pl",
"Portal Morski": "portalmorski.pl",
"Portal Obronny": "portalobronny.pl",
"Portal Samorządowy": "portalsamorzadowy.pl",
"Polska Morska": "polska-morska.pl",
"Rynek Infrastruktury": "rynekinfrastruktury.pl",
"Top-Oze": "top-oze.pl",
"FOCUS ON Business": "focusonbusiness.eu",
# Regionalne
"Dziennik Bałtycki": "dziennikbaltycki.pl",
"Głos Pomorza": "gp24.pl",
"Kaszuby24": "kaszuby24.pl",
"Nadmorski24": "nadmorski24.pl",
"Portal Kujawski": "portalkujawski.pl",
"Pracodawcy Pomorza": "pracodawcypomorza.pl",
"Rumia naturalnie pomysłowa": "rumia.eu",
"Tygodnik Bydgoski": "tygodnikbydgoski.pl",
"Zawsze Pomorze": "zawszepomorze.pl",
# Radio i TV
"Radio Gdańsk": "radiogdansk.pl",
"Radio Weekend FM": "weekendfm.pl",
"Polskie Radio 24": "polskieradio24.pl",
"Polskie Radio Koszalin": "prkoszalin.pl",
"TVP Gdańsk": "gdansk.tvp.pl",
"TVP Bydgoszcz": "bydgoszcz.tvp.pl",
"TVP Info": "tvp.info",
# Inne
"Polska Agencja Prasowa SA": "pap.pl",
"OKO.press": "oko.press",
}
def get_domain_favicon(domain: str) -> str:
"""Zwróć URL favicona przez Google API."""
return f"https://www.google.com/s2/favicons?domain={domain}&sz=128"
def extract_source_from_title(title: str) -> str | None:
"""Wyciągnij źródło z tytułu (po ostatnim ' - ')."""
if ' - ' not in title:
return None
return title.rsplit(' - ', 1)[-1].strip()
def main():
parser = argparse.ArgumentParser(description='Napraw źródła newsów z Google News')
parser.add_argument('--dry-run', action='store_true', help='Tryb testowy - nie zapisuj')
parser.add_argument('--limit', type=int, default=None, help='Limit newsów')
args = parser.parse_args()
print("=" * 70)
print("Google News Source Fixer")
print("=" * 70)
if args.dry_run:
print("🔍 TRYB TESTOWY - zmiany NIE będą zapisane\n")
engine = create_engine(DATABASE_URL)
Session = sessionmaker(bind=engine)
session = Session()
try:
from database import ZOPKNews
# Pobierz newsy z Google News z favicon
query = session.query(ZOPKNews).filter(
ZOPKNews.status.in_(['approved', 'auto_approved']),
ZOPKNews.source_domain == 'news.google.com',
ZOPKNews.image_url.like('%s2/favicons%')
).order_by(ZOPKNews.published_at.desc())
if args.limit:
query = query.limit(args.limit)
news_items = query.all()
print(f"📰 Znaleziono {len(news_items)} newsów do przetworzenia\n")
stats = {
'processed': 0,
'mapped': 0,
'unknown': 0,
'no_pattern': 0
}
unknown_sources = set()
for i, news in enumerate(news_items, 1):
source_name = extract_source_from_title(news.title)
if not source_name:
stats['no_pattern'] += 1
print(f"[{i}] ⚠ Brak wzorca ' - ' w tytule: {news.title[:50]}...")
continue
domain = SOURCE_TO_DOMAIN.get(source_name)
if domain:
stats['processed'] += 1
stats['mapped'] += 1
favicon_url = get_domain_favicon(domain)
if not args.dry_run:
news.source_domain = domain
news.image_url = favicon_url
session.commit()
print(f"[{i}] ✓ {source_name}{domain}")
else:
print(f"[{i}] [DRY-RUN] {source_name}{domain}")
else:
stats['unknown'] += 1
unknown_sources.add(source_name)
print(f"[{i}] ✗ Nieznane źródło: {source_name}")
print("\n" + "=" * 70)
print("PODSUMOWANIE")
print("=" * 70)
print(f"Przetworzono: {stats['processed']}")
print(f" - Zmapowane: {stats['mapped']}")
print(f" - Nieznane źródła: {stats['unknown']}")
print(f" - Brak wzorca w tytule: {stats['no_pattern']}")
if unknown_sources:
print(f"\n⚠ Nieznane źródła ({len(unknown_sources)}) - dodaj do SOURCE_TO_DOMAIN:")
for src in sorted(unknown_sources):
print(f' "{src}": "",')
if args.dry_run:
print("\n⚠️ To był tryb testowy. Uruchom bez --dry-run aby zapisać.")
except Exception as e:
print(f"❌ Błąd: {e}")
import traceback
traceback.print_exc()
session.rollback()
finally:
session.close()
if __name__ == '__main__':
main()