2026-02-28 09:20:56 +01:00
import asyncio
import os
import asyncpg
import smtplib
2026-03-02 19:39:40 +01:00
import re
2026-03-05 05:18:03 +01:00
import argparse
2026-02-28 09:20:56 +01:00
from datetime import datetime
from email . mime . text import MIMEText
from email . mime . multipart import MIMEMultipart
from playwright . async_api import async_playwright
try :
from playwright_stealth import stealth_async as apply_stealth
except ImportError :
from playwright_stealth import stealth as apply_stealth
2026-03-05 05:18:03 +01:00
from google import genai
2026-02-28 09:20:56 +01:00
from dotenv import load_dotenv
2026-04-27 08:56:58 +02:00
from course_status_history import ensure_course_status_history_table , log_course_status_change
2026-04-16 09:58:08 +02:00
from env_config import get_database_url
2026-05-04 15:30:29 +02:00
from scrape_utils import (
ProgressCallback ,
emit_progress ,
exclude_discontinued_facilities_clause ,
make_progress_event ,
)
2026-02-28 09:20:56 +01:00
load_dotenv ( )
2026-04-16 09:58:08 +02:00
DB_URL = get_database_url ( )
2026-05-04 15:30:29 +02:00
DISABLED_STATUS_METHODS = { " " , " disabled " , " manual " }
2026-02-28 09:20:56 +01:00
2026-03-05 05:18:03 +01:00
# ==========================================
# KONFIGURERER GEMINI AI (NY SDK)
# ==========================================
client = genai . Client ( )
2026-03-05 09:25:15 +01:00
async def ask_llm_status ( text , course_name , is_single_course , ai_instruction = None ) :
2026-03-05 05:18:03 +01:00
if is_single_course :
bane_instruks = " Finn den generelle banestatusen for dette golfanlegget. Se bort fra spesifikke banenavn, da anlegget kun har én bane. "
else :
bane_instruks = f ' Finn banestatusen SPESIFIKT for banen som heter/omtales som: " { course_name } " . '
2026-03-05 15:11:04 +01:00
ekstra_tekst = f " \n !!! VIKTIG EKSTRA-INSTRUKS FRA ADMIN (DENNE OVERSTYRER ALLE ANDRE REGLER) !!!: \n { ai_instruction } \n " if ai_instruction else " "
2026-03-05 09:25:15 +01:00
2026-03-05 05:18:03 +01:00
prompt = f """
Du er en ekspert på å lese norske golfklubbers nettsider for å finne banestatus .
{ bane_instruks }
2026-03-05 09:25:15 +01:00
{ ekstra_tekst }
2026-03-05 05:18:03 +01:00
Svar KUN med nøyaktig ETT av disse ordene :
- aapen ( hvis banen er åpen / sommergreener )
- stengt ( hvis banen er lukket / stengt / frost / snø )
- aapen_med_vintergreener ( hvis det spilles på vintergreener )
- aapner_snart ( hvis den åpner om kort tid )
- stenger_snart ( hvis den stenger for sesongen om kort tid )
- under_utvikling ( hvis den er under utvikling )
- nedlagt ( hvis den er nedlagt )
- ukjent ( hvis du ikke finner noe info om banen i teksten )
Tekst fra nettsiden :
{ text [ : 15000 ] }
"""
2026-03-05 15:11:04 +01:00
print ( " \n " + " = " * 60 )
print ( f " 🤖 SENDER PROMPT TIL GEMINI FOR: ' { course_name } ' " )
print ( f " 👉 STANDARD-INSTRUKS: { bane_instruks } " )
if ai_instruction :
print ( f " 👉 ADMIN-HVISKER: { ai_instruction } " )
clean_text_sample = " " . join ( text . split ( ) ) [ : 250 ]
print ( f " 👉 TEKST FRA NETTSIDEN (utdrag): ' { clean_text_sample } ... ' " )
print ( " = " * 60 + " \n " )
2026-03-05 05:18:03 +01:00
try :
response = await client . aio . models . generate_content (
model = ' gemini-2.5-flash ' ,
contents = prompt
)
svar = response . text . strip ( ) . lower ( )
2026-03-05 15:11:04 +01:00
print ( f " 🧠 GEMINI RÅ-SVAR: ' { svar } ' " )
# --- NYTT: SORTERT SIKKERHETSFILTER ---
2026-03-05 05:18:03 +01:00
gyldige_svar = [
" aapen_med_vintergreener " ,
" aapner_snart " ,
" stenger_snart " ,
" under_utvikling " ,
" nedlagt " ,
2026-03-05 15:11:04 +01:00
" stengt " ,
" aapen " ,
2026-03-05 05:18:03 +01:00
" ukjent "
]
for gyldig in gyldige_svar :
if gyldig in svar :
return gyldig
return " ukjent "
except Exception as e :
print ( f " ❌ Gemini Feil: { e } " )
return " ukjent "
# ==========================================
# EKSISTERENDE LOGIKK FOR MANUELL SCRAPING
# ==========================================
2026-02-28 09:20:56 +01:00
def clean_text ( text ) :
return re . sub ( r ' [^a-zA-Z0-9æøåÆØÅ] ' , ' ' , text ) . lower ( )
def interpret_status ( text , keyword = None ) :
t_raw = text . lower ( )
if keyword :
k_clean = clean_text ( keyword )
if k_clean not in clean_text ( t_raw ) :
return " NOT_FOUND "
parts = re . split ( re . escape ( keyword ) , t_raw , flags = re . IGNORECASE )
if len ( parts ) > 1 :
t_raw = parts [ 1 ] [ : 150 ]
else :
2026-03-02 19:39:40 +01:00
t_raw = t_raw [ - 200 : ]
2026-02-28 09:20:56 +01:00
if any ( word in t_raw for word in [ " stengt " , " lukket " , " frost " , " snø " , " is " , " closed " , " stenger " ] ) :
return " stengt "
if any ( word in t_raw for word in [ " vintergreen " , " vintergrønn " , " vinter " ] ) :
return " aapen_med_vintergreener "
if any ( word in t_raw for word in [ " snart " , " åpner kl " ] ) :
return " aapner_snart "
if any ( word in t_raw for word in [ " åpen " , " åpent " , " aapen " , " open " ] ) :
return " aapen "
return " ukjent "
2026-03-02 19:39:40 +01:00
def send_report ( changes , warnings , successes ) :
if not changes and not warnings and not successes : return
2026-02-28 09:20:56 +01:00
subject = f " TeeOff Banestatus Rapport - { datetime . now ( ) . strftime ( ' %d . % m. % Y ' ) } "
2026-03-05 05:18:03 +01:00
2026-02-28 09:20:56 +01:00
body = " BANESTATUS RAPPORT \n " + " = " * 30 + " \n \n "
2026-03-02 19:39:40 +01:00
2026-02-28 09:20:56 +01:00
if changes : body + = " ✅ OPPDATERINGER: \n " + " \n " . join ( changes ) + " \n \n "
2026-03-02 19:39:40 +01:00
if warnings : body + = " ⚠️ MERKNADER / ADVARSLER: \n " + " \n " . join ( warnings ) + " \n \n "
if successes : body + = " 🆗 VELLYKKEDE SJEKKER (INGEN ENDRING): \n " + " \n " . join ( successes ) + " \n "
2026-02-28 09:20:56 +01:00
2026-03-02 19:39:40 +01:00
msg = MIMEMultipart ( )
msg [ ' From ' ] = os . getenv ( " SMTP_USER " )
msg [ ' To ' ] = os . getenv ( " EMAIL_TO " )
msg [ ' Subject ' ] = subject
2026-02-28 09:20:56 +01:00
msg . attach ( MIMEText ( body , ' plain ' ) )
try :
with smtplib . SMTP_SSL ( os . getenv ( " SMTP_SERVER " ) , int ( os . getenv ( " SMTP_PORT " ) ) ) as server :
server . login ( os . getenv ( " SMTP_USER " ) , os . getenv ( " SMTP_PASS " ) )
server . send_message ( msg )
print ( " ✅ Rapport sendt på e-post. " )
2026-03-02 19:39:40 +01:00
except Exception as e :
print ( f " ❌ E-post feil: { e } " )
2026-02-28 09:20:56 +01:00
2026-03-05 05:18:03 +01:00
# ==========================================
# HOVEDMOTOR
# ==========================================
2026-04-12 10:11:23 +02:00
async def run_daily_scraping ( facility_ids = None , progress_callback : ProgressCallback | None = None ) :
2026-02-28 09:20:56 +01:00
print ( f " 🚀 Starter sjekk { datetime . now ( ) . strftime ( ' % H: % M: % S ' ) } ... " )
conn = await asyncpg . connect ( DB_URL )
2026-04-27 08:56:58 +02:00
await ensure_course_status_history_table ( conn )
2026-05-04 15:30:29 +02:00
facility_filter = exclude_discontinued_facilities_clause ( " facilities " )
2026-02-28 09:20:56 +01:00
2026-03-05 05:18:03 +01:00
if facility_ids :
print ( f " 📌 Kjører skraping KUN for anlegg-ID(er): { facility_ids } " )
facilities = await conn . fetch (
2026-05-04 15:30:29 +02:00
f " SELECT id, name, scrape_status_url, scrape_status_selector, scrape_method, ai_instruction FROM facilities WHERE scrape_status_url IS NOT NULL AND id = ANY($1::int[]) { facility_filter } " ,
2026-03-05 05:18:03 +01:00
facility_ids
)
else :
print ( " 🌍 Kjører skraping for ALLE anlegg med scrape_status_url... " )
facilities = await conn . fetch (
2026-05-04 15:30:29 +02:00
f " SELECT id, name, scrape_status_url, scrape_status_selector, scrape_method, ai_instruction FROM facilities WHERE scrape_status_url IS NOT NULL { facility_filter } "
2026-03-05 05:18:03 +01:00
)
if not facilities :
print ( " ⚠️ Fant ingen anlegg å skrape. " )
await conn . close ( )
2026-04-10 18:37:33 +02:00
return {
" processed_facilities " : 0 ,
" updated_courses " : 0 ,
" warnings " : 0 ,
" successes " : 0 ,
2026-04-12 10:11:23 +02:00
" failed_facilities " : 0 ,
" skipped_facilities " : 0 ,
2026-04-10 18:37:33 +02:00
}
2026-03-05 05:18:03 +01:00
2026-03-02 19:39:40 +01:00
changes , warnings , successes = [ ] , [ ] , [ ]
2026-04-12 10:11:23 +02:00
total_facilities = len ( facilities )
ok_facilities = 0
failed_facilities = 0
skipped_facilities = 0
await emit_progress (
progress_callback ,
progress_total = total_facilities ,
progress_completed = 0 ,
progress_ok = 0 ,
progress_failed = 0 ,
progress_skipped = 0 ,
event = make_progress_event (
facility_id = None ,
facility_name = " Banestatus " ,
outcome = " info " ,
message = f " Starter banestatusskraping for { total_facilities } anlegg. " ,
processed = 0 ,
total = total_facilities ,
) ,
)
2026-02-28 09:20:56 +01:00
async with async_playwright ( ) as p :
browser = await p . chromium . launch ( headless = True )
context = await browser . new_context ( )
2026-04-12 10:11:23 +02:00
for index , f in enumerate ( facilities , start = 1 ) :
2026-05-04 15:30:29 +02:00
raw_method = ( f . get ( ' scrape_method ' ) or " " ) . strip ( )
method = raw_method or ' css_selector '
method_label = " disabled " if raw_method in { " " , " disabled " } else method
2026-04-12 10:11:23 +02:00
facility_id = f [ ' id ' ]
facility_name = f [ ' name ' ]
await emit_progress (
progress_callback ,
current_facility_id = facility_id ,
current_facility_name = facility_name ,
event = make_progress_event (
facility_id = facility_id ,
facility_name = facility_name ,
outcome = " info " ,
2026-05-04 15:30:29 +02:00
message = f " Starter sjekk med metode { method_label } . " ,
2026-04-12 10:11:23 +02:00
processed = index - 1 ,
total = total_facilities ,
) ,
)
2026-03-05 09:25:15 +01:00
2026-05-04 15:30:29 +02:00
if raw_method in DISABLED_STATUS_METHODS :
skip_reason = " Manuell overstyring " if raw_method == " manual " else " Skraping avslått "
progress_message = (
" Hoppet over fordi anlegget er satt til manuell overstyring. "
if raw_method == " manual "
else " Hoppet over fordi banestatusskraping er avslått for anlegget. "
)
successes . append ( f " ⏸️ { f [ ' name ' ] } : Hoppet over ( { skip_reason } ) " )
print ( f " ⏸️ Hopper over skraping av { f [ ' name ' ] } ( { skip_reason } ) " )
2026-04-12 10:11:23 +02:00
skipped_facilities + = 1
await emit_progress (
progress_callback ,
progress_completed = index ,
progress_ok = ok_facilities ,
progress_failed = failed_facilities ,
progress_skipped = skipped_facilities ,
current_facility_id = facility_id ,
current_facility_name = facility_name ,
event = make_progress_event (
facility_id = facility_id ,
facility_name = facility_name ,
outcome = " warning " ,
2026-05-04 15:30:29 +02:00
message = progress_message ,
2026-04-12 10:11:23 +02:00
processed = index ,
total = total_facilities ,
) ,
)
2026-03-05 09:25:15 +01:00
continue
2026-02-28 09:20:56 +01:00
page = await context . new_page ( )
try : await apply_stealth ( page )
except : pass
try :
2026-03-05 09:25:15 +01:00
print ( f " 🔍 Besøker { f [ ' name ' ] } (Metode: { method } )... " )
2026-03-02 19:39:40 +01:00
await page . goto ( f [ ' scrape_status_url ' ] , timeout = 60000 , wait_until = " domcontentloaded " )
2026-03-05 17:44:18 +01:00
await page . wait_for_timeout ( 3000 )
2026-02-28 09:20:56 +01:00
2026-03-02 19:39:40 +01:00
full_text = " "
if method == ' css_selector ' :
element = page . locator ( f [ ' scrape_status_selector ' ] ) . first
if await element . count ( ) == 0 :
warnings . append ( f " ❌ { f [ ' name ' ] } : Fant ikke CSS-elementet ' { f [ ' scrape_status_selector ' ] } ' " )
2026-04-12 10:11:23 +02:00
failed_facilities + = 1
await emit_progress (
progress_callback ,
progress_completed = index ,
progress_ok = ok_facilities ,
progress_failed = failed_facilities ,
progress_skipped = skipped_facilities ,
current_facility_id = facility_id ,
current_facility_name = facility_name ,
event = make_progress_event (
facility_id = facility_id ,
facility_name = facility_name ,
outcome = " error " ,
message = f " Fant ikke CSS-elementet { f [ ' scrape_status_selector ' ] } . " ,
processed = index ,
total = total_facilities ,
) ,
)
2026-03-02 19:39:40 +01:00
continue
full_text = await element . inner_text ( )
elif method == ' iframe_golfbox ' :
frame = page . frame_locator ( ' iframe[src*= " golfbox " ] ' )
element = frame . locator ( f [ ' scrape_status_selector ' ] ) . first
if await element . count ( ) == 0 :
warnings . append ( f " ❌ { f [ ' name ' ] } : Fant ikke elementet ' { f [ ' scrape_status_selector ' ] } ' i iframen " )
2026-04-12 10:11:23 +02:00
failed_facilities + = 1
await emit_progress (
progress_callback ,
progress_completed = index ,
progress_ok = ok_facilities ,
progress_failed = failed_facilities ,
progress_skipped = skipped_facilities ,
current_facility_id = facility_id ,
current_facility_name = facility_name ,
event = make_progress_event (
facility_id = facility_id ,
facility_name = facility_name ,
outcome = " error " ,
message = f " Fant ikke elementet { f [ ' scrape_status_selector ' ] } i Golfbox-iframe. " ,
processed = index ,
total = total_facilities ,
) ,
)
2026-03-02 19:39:40 +01:00
continue
full_text = await element . inner_text ( )
2026-03-04 13:17:10 +01:00
elif method == ' click_then_css ' :
parts = f [ ' scrape_status_selector ' ] . split ( ' || ' )
if len ( parts ) != 2 :
warnings . append ( f " ❌ { f [ ' name ' ] } : Ugyldig selector for click_then_css (mangler ||) " )
2026-04-12 10:11:23 +02:00
failed_facilities + = 1
await emit_progress (
progress_callback ,
progress_completed = index ,
progress_ok = ok_facilities ,
progress_failed = failed_facilities ,
progress_skipped = skipped_facilities ,
current_facility_id = facility_id ,
current_facility_name = facility_name ,
event = make_progress_event (
facility_id = facility_id ,
facility_name = facility_name ,
outcome = " error " ,
message = " Ugyldig click_then_css-selector i konfigurasjonen. " ,
processed = index ,
total = total_facilities ,
) ,
)
2026-03-04 13:17:10 +01:00
continue
btn_selector , text_selector = parts
btn = page . locator ( btn_selector ) . first
if await btn . count ( ) == 0 :
warnings . append ( f " ❌ { f [ ' name ' ] } : Fant ikke knappen å klikke på: ' { btn_selector } ' " )
2026-04-12 10:11:23 +02:00
failed_facilities + = 1
await emit_progress (
progress_callback ,
progress_completed = index ,
progress_ok = ok_facilities ,
progress_failed = failed_facilities ,
progress_skipped = skipped_facilities ,
current_facility_id = facility_id ,
current_facility_name = facility_name ,
event = make_progress_event (
facility_id = facility_id ,
facility_name = facility_name ,
outcome = " error " ,
message = f " Fant ikke knappen { btn_selector } som skulle klikkes. " ,
processed = index ,
total = total_facilities ,
) ,
)
2026-03-04 13:17:10 +01:00
continue
2026-03-05 17:44:18 +01:00
await btn . click ( force = True )
await page . wait_for_timeout ( 2000 )
2026-03-04 13:17:10 +01:00
element = page . locator ( text_selector ) . first
if await element . count ( ) == 0 :
warnings . append ( f " ❌ { f [ ' name ' ] } : Fant ikke tekstboksen ' { text_selector } ' etter klikk " )
2026-04-12 10:11:23 +02:00
failed_facilities + = 1
await emit_progress (
progress_callback ,
progress_completed = index ,
progress_ok = ok_facilities ,
progress_failed = failed_facilities ,
progress_skipped = skipped_facilities ,
current_facility_id = facility_id ,
current_facility_name = facility_name ,
event = make_progress_event (
facility_id = facility_id ,
facility_name = facility_name ,
outcome = " error " ,
message = f " Fant ikke tekstboksen { text_selector } etter klikk. " ,
processed = index ,
total = total_facilities ,
) ,
)
2026-03-04 13:17:10 +01:00
continue
full_text = await element . inner_text ( )
2026-02-28 09:20:56 +01:00
2026-03-05 05:18:03 +01:00
elif method == ' llm_parse ' :
2026-03-05 15:11:04 +01:00
print ( " 🖱️ Leter etter knapper å klikke på for å avdekke skjult tekst... " )
2026-03-05 17:44:18 +01:00
knapper = await page . get_by_text ( re . compile ( r " banestatus|dagens status|se status|se dagens status|baneinfo| \ bstatus \ b " , re . IGNORECASE ) ) . all ( )
2026-03-05 05:18:03 +01:00
2026-03-05 17:44:18 +01:00
klikk_count = 0
2026-03-05 05:18:03 +01:00
for knapp in knapper :
try :
if await knapp . is_visible ( ) :
2026-03-05 17:44:18 +01:00
await knapp . click ( timeout = 2000 , force = True )
klikk_count + = 1
await page . wait_for_timeout ( 2000 )
2026-03-05 05:18:03 +01:00
except Exception :
pass
2026-03-05 17:44:18 +01:00
if klikk_count > 0 :
print ( f " 🎯 Tvangsklikket på { klikk_count } status-knapp(er)! Venter ekstra på at innholdet laster... " )
await page . wait_for_timeout ( 2000 )
else :
print ( " ⚠️ Fant ingen knapper å klikke på. " )
# --- NYTT: HENTER OGSÅ SKJULT TEKST (For Scangolf megamenyer) ---
2026-03-05 05:18:03 +01:00
element = page . locator ( " body " ) . first
if await element . count ( ) == 0 :
warnings . append ( f " ❌ { f [ ' name ' ] } : Klarte ikke å lese siden for AI-tolkning " )
2026-04-12 10:11:23 +02:00
failed_facilities + = 1
await emit_progress (
progress_callback ,
progress_completed = index ,
progress_ok = ok_facilities ,
progress_failed = failed_facilities ,
progress_skipped = skipped_facilities ,
current_facility_id = facility_id ,
current_facility_name = facility_name ,
event = make_progress_event (
facility_id = facility_id ,
facility_name = facility_name ,
outcome = " error " ,
message = " Klarte ikke å lese siden for AI-tolkning. " ,
processed = index ,
total = total_facilities ,
) ,
)
2026-03-05 05:18:03 +01:00
continue
2026-03-05 17:44:18 +01:00
synlig_tekst = await element . inner_text ( ) or " "
skjult_tekst = await element . text_content ( ) or " "
# Slår sammen all tekst slik at Gemini får med seg menyer som er gjemt med CSS
råtekst = synlig_tekst + " " + skjult_tekst
2026-03-05 05:18:03 +01:00
full_text = " " . join ( råtekst . split ( ) )
2026-03-05 17:44:18 +01:00
# ----------------------------------------------------------------
2026-03-05 05:18:03 +01:00
2026-03-02 19:39:40 +01:00
else :
warnings . append ( f " ⚠️ { f [ ' name ' ] } : Ukjent skrapemetode i databasen: ' { method } ' " )
2026-04-12 10:11:23 +02:00
failed_facilities + = 1
await emit_progress (
progress_callback ,
progress_completed = index ,
progress_ok = ok_facilities ,
progress_failed = failed_facilities ,
progress_skipped = skipped_facilities ,
current_facility_id = facility_id ,
current_facility_name = facility_name ,
event = make_progress_event (
facility_id = facility_id ,
facility_name = facility_name ,
outcome = " error " ,
message = f " Ukjent skrapemetode: { method } . " ,
processed = index ,
total = total_facilities ,
) ,
)
2026-03-02 19:39:40 +01:00
continue
2026-02-28 09:20:56 +01:00
await conn . execute ( " UPDATE facilities SET status_updated_at = CURRENT_DATE WHERE id = $1 " , f [ ' id ' ] )
courses = await conn . fetch ( " SELECT id, name, status, scrape_keyword FROM courses WHERE facility_id = $1 " , f [ ' id ' ] )
2026-03-05 05:18:03 +01:00
is_single_course = len ( courses ) == 1
2026-04-12 10:11:23 +02:00
facility_changed = 0
facility_confirmed = 0
facility_unresolved = 0
2026-03-05 05:18:03 +01:00
2026-02-28 09:20:56 +01:00
for c in courses :
2026-03-05 17:44:18 +01:00
old_status = c [ ' status ' ] or " ukjent "
2026-03-05 05:18:03 +01:00
if method == ' llm_parse ' :
print ( f " 🤖 Spør Gemini om status for ' { c [ ' name ' ] } ' (Singelbane: { is_single_course } )... " )
2026-03-05 09:25:15 +01:00
new_status = await ask_llm_status ( full_text , c [ ' name ' ] , is_single_course , f . get ( ' ai_instruction ' ) )
2026-03-05 15:11:04 +01:00
print ( " ⏳ Tar 5 sekunders pause for å spare Gemini-kvoten... " )
await asyncio . sleep ( 5 )
2026-03-05 05:18:03 +01:00
else :
new_status = interpret_status ( full_text , c [ ' scrape_keyword ' ] )
2026-02-28 09:20:56 +01:00
if new_status == " NOT_FOUND " :
2026-03-05 05:18:03 +01:00
warnings . append ( f " ❓ { f [ ' name ' ] } ( { c [ ' name ' ] } ): Fant ikke søkeordet ' { c [ ' scrape_keyword ' ] } ' i teksten. " )
2026-04-12 10:11:23 +02:00
facility_unresolved + = 1
2026-02-28 09:20:56 +01:00
continue
2026-03-05 17:44:18 +01:00
# --- OPPDATERT LOGIKK (Fikser logg-buggen) ---
if new_status == " ukjent " :
# Sikkerhetsnettet slår inn: Vi beholder gammel status!
warnings . append ( f " ⚠️ { f [ ' name ' ] } ( { c [ ' name ' ] } ): Fant ikke status. Beholder ' { old_status . upper ( ) } ' . " )
print ( f " 🟡 KONKLUSJON: Fant ikke status i teksten (Sikkerhetsnett). Beholder gammel status ( { old_status . upper ( ) } ). " )
2026-04-12 10:11:23 +02:00
facility_unresolved + = 1
2026-03-05 17:44:18 +01:00
elif new_status != old_status :
2026-04-27 08:56:58 +02:00
await log_course_status_change (
conn ,
course_id = int ( c [ " id " ] ) ,
facility_id = int ( f [ " id " ] ) ,
old_status = old_status ,
new_status = new_status ,
change_source = " scraper " ,
changed_by = " scraper " ,
)
2026-02-28 09:20:56 +01:00
await conn . execute ( " UPDATE courses SET status = $1 WHERE id = $2 " , new_status , c [ ' id ' ] )
changes . append ( f " 🔹 { f [ ' name ' ] } ( { c [ ' name ' ] } ): { old_status . upper ( ) } ➔ { new_status . upper ( ) } " )
2026-03-05 15:11:04 +01:00
print ( f " 🟢 KONKLUSJON: Status endret fra { old_status . upper ( ) } til { new_status . upper ( ) } " )
2026-04-12 10:11:23 +02:00
facility_changed + = 1
2026-02-28 09:20:56 +01:00
else :
2026-03-02 19:39:40 +01:00
successes . append ( f " ✅ { f [ ' name ' ] } ( { c [ ' name ' ] } ): { new_status . upper ( ) } " )
2026-03-05 17:44:18 +01:00
print ( f " ⚪ KONKLUSJON: Ingen endring. Banen er fortsatt { old_status . upper ( ) } " )
2026-04-12 10:11:23 +02:00
facility_confirmed + = 1
2026-03-05 17:44:18 +01:00
# ---------------------------------------------
2026-02-28 09:20:56 +01:00
2026-04-12 10:11:23 +02:00
ok_facilities + = 1
if facility_changed > 0 :
facility_outcome = " success "
facility_message = (
f " { facility_changed } baner oppdatert, { facility_confirmed } bekreftet "
+ ( f " , { facility_unresolved } beholdt som før " if facility_unresolved > 0 else " " )
+ " . "
)
elif facility_unresolved > 0 :
facility_outcome = " warning "
facility_message = (
f " Ingen statusendring. { facility_confirmed } baner bekreftet og "
f " { facility_unresolved } beholdt som før. "
)
else :
facility_outcome = " success "
facility_message = f " Ingen endring. { facility_confirmed } baner bekreftet. "
await emit_progress (
progress_callback ,
progress_completed = index ,
progress_ok = ok_facilities ,
progress_failed = failed_facilities ,
progress_skipped = skipped_facilities ,
current_facility_id = facility_id ,
current_facility_name = facility_name ,
event = make_progress_event (
facility_id = facility_id ,
facility_name = facility_name ,
outcome = facility_outcome ,
message = facility_message ,
processed = index ,
total = total_facilities ,
) ,
)
2026-02-28 09:20:56 +01:00
except Exception as e :
2026-03-02 19:39:40 +01:00
err_msg = str ( e ) . split ( ' \n ' ) [ 0 ]
warnings . append ( f " 🔥 { f [ ' name ' ] } : Feil under skraping: { err_msg } " )
2026-04-12 10:11:23 +02:00
failed_facilities + = 1
await emit_progress (
progress_callback ,
progress_completed = index ,
progress_ok = ok_facilities ,
progress_failed = failed_facilities ,
progress_skipped = skipped_facilities ,
current_facility_id = facility_id ,
current_facility_name = facility_name ,
event = make_progress_event (
facility_id = facility_id ,
facility_name = facility_name ,
outcome = " error " ,
message = f " Feil under skraping: { err_msg } " ,
processed = index ,
total = total_facilities ,
) ,
)
2026-02-28 09:20:56 +01:00
finally :
await page . close ( )
2026-03-05 05:18:03 +01:00
2026-02-28 09:20:56 +01:00
await browser . close ( )
await conn . close ( )
2026-03-02 19:39:40 +01:00
send_report ( changes , warnings , successes )
2026-02-28 09:20:56 +01:00
print ( " 🏁 Ferdig. " )
2026-04-10 18:37:33 +02:00
return {
" processed_facilities " : len ( facilities ) ,
" updated_courses " : len ( changes ) ,
" warnings " : len ( warnings ) ,
" successes " : len ( successes ) ,
2026-04-12 10:11:23 +02:00
" failed_facilities " : failed_facilities ,
" skipped_facilities " : skipped_facilities ,
2026-04-10 18:37:33 +02:00
}
2026-02-28 09:20:56 +01:00
if __name__ == " __main__ " :
2026-03-05 05:18:03 +01:00
parser = argparse . ArgumentParser ( description = " TeeOff Status Scraper " )
parser . add_argument ( " --ids " , type = str , help = " Kommaseparert liste med anleggs-IDer " , default = None )
args = parser . parse_args ( )
facility_ids_list = None
if args . ids :
try :
facility_ids_list = [ int ( id_str . strip ( ) ) for id_str in args . ids . split ( " , " ) if id_str . strip ( ) ]
except ValueError :
print ( " ❌ Feil format på --ids. Må være kommaseparerte tall, f.eks: 1,4,12 " )
exit ( 1 )
2026-04-10 18:37:33 +02:00
asyncio . run ( run_daily_scraping ( facility_ids_list ) )