About Marvia.es

Marvia.es shows live and scheduled Renfe Cercanias Malaga C1/C2 train information for the Malaga rail network.

The Next train view focuses on one station at a time. When Renfe live data exists for an approaching train, it shows the official broadcast ETA and live data freshness.

When the station read path cannot produce a live ETA row, it falls back to the static timetable so the next scheduled C1/C2 departures are still visible. The Train map view plots live GPS train locations and, for a selected station, uses the same displayed ETA providers as the station snapshot endpoint.

Data Sources

Marvia.es consumes Renfe Operadora static GTFS and GTFS-Realtime feeds directly for the Malaga C1/C2 scope.

Attribution
Origen de los datos: Renfe Operadora
Static GTFS snapshot
Last update unavailable
License
CC-BY 4.0

Marvia.es is an unofficial app and is not affiliated with, endorsed by, or sponsored by Renfe. The open-data license covers reuse of the published data, not Renfe trademarks, wordmarks, or logos. This site does not use the Renfe wordmark or logo in a way that implies endorsement.

Open Data Sources / Fuentes de datos

Data Flow

The realtime poller fetches national Renfe feeds, narrows them to the C1/C2 Malaga scope before persistence, and stores filtered raw GTFS-Realtime rows for the web read path.

Renfe source feeds through the Malaga-scoped poller into web pages Trip updates, vehicle positions, alerts, and static GTFS feed a Malaga-only poller. The poller writes filtered rows to SQLite or DuckDB local storage, or PostgreSQL through ADBC. FastAPI reads the store for Next train and Map pages. Renfe publisher inputs trip_updates.json broadcast stop-time updates vehicle_positions.json live train position fixes alerts.json service alerts retained in scope static GTFS zip routes, stops, trips, shapes Malaga-only capture boundary polite poller 20s live cadence staggered feed tasks C1/C2 filter trip_id, stop_id, label unfiltered national entities are dropped hot store SQLite or DuckDB PostgreSQL via ADBC raw_responses_realtime filtered body_json filtered raw_payload static snapshots cached on local disk web read path FastAPI routes station, snapshot, map APIs Next train station ETA columns Train map markers and countdowns

Persisted Table Schema

Discovery scanned the persisted table definitions in code: ADBC hot-store SQL in src/marvia/store/hot.py, the historical DuckDB schema in src/marvia/store.py, and the Iceberg table creator in src/marvia/warehouse.py. Indexes, sequences, and query-engine temporary views are not row tables, so they are not listed as persisted tables.

  • raw_responses_realtime, arrivals_board, arrivals_board_eta_results, and hot_store_flush_watermarks are created by the SQLite/PostgreSQL ADBC hot store.
  • raw_responses_realtime, arrivals_board, arrivals_board_eta_results, and static_gtfs_metadata are created by the historical DuckDB store.
  • marvia.raw_responses_realtime is created in the Iceberg warehouse with two additional partition columns.

raw_responses_realtime and Iceberg marvia.raw_responses_realtime

Sources: hot-store SQL in src/marvia/store/hot.py, DuckDB SCHEMA_SQL in src/marvia/store.py, and Iceberg schema/partition spec in src/marvia/warehouse.py.

Column Real type(s) Meaning
idSQLite INTEGER PRIMARY KEY AUTOINCREMENT; PostgreSQL BIGSERIAL PRIMARY KEY; DuckDB BIGINT PRIMARY KEY DEFAULT nextval(...); Iceberg LongType requiredMonotonic persisted row identifier; the cold flush carries this id into Iceberg.
feed_kindSQLite/PostgreSQL TEXT; DuckDB VARCHAR; Iceberg StringType required; constrained to trip_updates, vehicle_positions, or alertsWhich GTFS-Realtime feed produced the row.
captured_atSQLite TEXT; PostgreSQL/DuckDB TIMESTAMPTZ; Iceberg TimestamptzType requiredUTC time when the poll response was captured.
feed_header_timestampSQLite TEXT; PostgreSQL/DuckDB TIMESTAMPTZ; Iceberg TimestamptzTypePublisher timestamp from the realtime feed header when Renfe sends one.
request_urlSQLite/PostgreSQL TEXT; DuckDB VARCHAR; Iceberg StringType requiredExact Renfe feed URL requested.
status_codeSQLite/PostgreSQL INTEGER with HTTP-range check; DuckDB USMALLINT; Iceberg IntegerType requiredHTTP response status for the poll.
body_jsonSQLite TEXT; PostgreSQL JSONB; DuckDB JSON; Iceberg StringType requiredJSON response body after the Malaga C1/C2 capture filter is applied.
raw_payloadSQLite/PostgreSQL TEXT; DuckDB VARCHAR; Iceberg StringType requiredRaw payload text after the same Malaga C1/C2 filter; used to preserve filtered raw fidelity.
latency_msSQLite REAL; PostgreSQL DOUBLE PRECISION; DuckDB DOUBLE; Iceberg DoubleType requiredMeasured request latency in milliseconds.
http_headers_jsonSQLite TEXT; PostgreSQL JSONB; DuckDB JSON; Iceberg StringType requiredPersisted HTTP response headers as JSON.
captured_dateIceberg-only StringType; Arrow writer marks it non-nullUTC date partition key derived from captured_at as YYYY-MM-DD.
captured_hourIceberg-only IntegerType; Arrow writer marks it non-nullUTC hour partition key derived from captured_at, from 0 through 23.

arrivals_board

Sources: ADBC hot-store SQL in src/marvia/store/hot.py and DuckDB SCHEMA_SQL in src/marvia/store.py.

Column Real type(s) Meaning
idSQLite INTEGER PRIMARY KEY AUTOINCREMENT; PostgreSQL BIGSERIAL PRIMARY KEY; DuckDB BIGINT PRIMARY KEY DEFAULT nextval(...)Persisted board row id referenced by provider result rows.
stop_idSQLite/PostgreSQL TEXT; DuckDB VARCHARStation stop id whose board includes this arrival.
direction_idSQLite/PostgreSQL TEXT; DuckDB VARCHARBoard direction bucket for the station.
train_idSQLite/PostgreSQL TEXT; DuckDB VARCHARTrain or trip identity used for the visible arrival row.
scheduled_arrival_atSQLite TEXT; PostgreSQL/DuckDB TIMESTAMPTZScheduled arrival timestamp for this train at this stop; part of the uniqueness key.
rankSQLite/PostgreSQL INTEGER checked between 1 and 3; DuckDB USMALLINT checked between 1 and 3Published order within a station and direction.
scheduled_atSQLite TEXT; PostgreSQL/DuckDB TIMESTAMPTZScheduled timestamp used by the board computation for sorting and display.
delay_secondsINTEGERDelay reported or computed for the candidate, in seconds, when available.
feed_captured_atSQLite TEXT; PostgreSQL/DuckDB TIMESTAMPTZOldest relevant provider feed capture time attached to the board row.
computed_atSQLite TEXT; PostgreSQL/DuckDB TIMESTAMPTZTime when the arrivals-board snapshot was computed.
location_unknownSQLite INTEGER checked 0/1; PostgreSQL/DuckDB BOOLEANWhether the recompute could not locate the train between known stations.
location_unavailable_reasonSQLite/PostgreSQL TEXT; DuckDB VARCHARReason an unknown current location is unavailable: NOT_YET_DEPARTED, TRIP_COMPLETED, or NO_SCHEDULE_MATCH.
at_station_idSQLite/PostgreSQL TEXT; DuckDB VARCHARCurrent station stop id when the train is known to be at a station.
at_station_nameSQLite/PostgreSQL TEXT; DuckDB VARCHARDisplay name for at_station_id.
from_station_idSQLite/PostgreSQL TEXT; DuckDB VARCHARPrevious station stop id when the train is between stations.
from_station_nameSQLite/PostgreSQL TEXT; DuckDB VARCHARDisplay name for from_station_id.
to_station_idSQLite/PostgreSQL TEXT; DuckDB VARCHARNext station stop id when the train is between stations.
to_station_nameSQLite/PostgreSQL TEXT; DuckDB VARCHARDisplay name for to_station_id.
current_timing_event_typeSQLite/PostgreSQL TEXT; DuckDB VARCHARCurrent timing signal event type from the board computation.
current_timing_stop_idSQLite/PostgreSQL TEXT; DuckDB VARCHARStop id associated with the current timing signal.
current_timing_stop_nameSQLite/PostgreSQL TEXT; DuckDB VARCHARDisplay name for current_timing_stop_id.
current_timing_scheduled_atSQLite TEXT; PostgreSQL/DuckDB TIMESTAMPTZScheduled timestamp for the current timing signal.
current_timing_observed_atSQLite TEXT; PostgreSQL/DuckDB TIMESTAMPTZObserved timestamp for the current timing signal.
current_timing_on_timeSQLite INTEGER checked 0/1; PostgreSQL/DuckDB BOOLEANWhether the current timing signal is within the configured on-time threshold.
current_timing_minutes_lateINTEGERCurrent timing lateness rounded to minutes.
current_timing_unavailable_reasonSQLite/PostgreSQL TEXT; DuckDB VARCHARReason current timing data could not be produced.
broadcast_schedule_stop_idSQLite/PostgreSQL TEXT; DuckDB VARCHARStop id for the broadcast schedule signal.
broadcast_schedule_stop_nameSQLite/PostgreSQL TEXT; DuckDB VARCHARDisplay name for broadcast_schedule_stop_id.
broadcast_schedule_scheduled_arrival_atSQLite TEXT; PostgreSQL/DuckDB TIMESTAMPTZScheduled arrival time paired with the official broadcast ETA.
broadcast_schedule_eta_atSQLite TEXT; PostgreSQL/DuckDB TIMESTAMPTZOfficial broadcast ETA timestamp from Renfe when available.
broadcast_schedule_on_timeSQLite INTEGER checked 0/1; PostgreSQL/DuckDB BOOLEANWhether the broadcast schedule signal is considered on time.
broadcast_schedule_minutes_lateINTEGERBroadcast schedule lateness rounded to minutes.
broadcast_schedule_unavailable_reasonSQLite/PostgreSQL TEXT; DuckDB VARCHARReason broadcast schedule data could not be produced.

arrivals_board_eta_results

Sources: ADBC hot-store SQL in src/marvia/store/hot.py and DuckDB SCHEMA_SQL in src/marvia/store.py.

Column Real type(s) Meaning
idSQLite INTEGER PRIMARY KEY AUTOINCREMENT; PostgreSQL BIGSERIAL PRIMARY KEY; DuckDB BIGINT PRIMARY KEY DEFAULT nextval(...)Persisted provider-result row id.
arrival_board_idSQLite INTEGER; PostgreSQL/DuckDB BIGINT; foreign key to arrivals_board(id)Parent arrivals-board row this provider result belongs to.
provider_nameSQLite/PostgreSQL TEXT; DuckDB VARCHARETA provider name, such as broadcast or physics.
eta_atSQLite TEXT; PostgreSQL/DuckDB TIMESTAMPTZProvider-predicted arrival timestamp; required unless the row is marked declined.
confidenceSQLite REAL; PostgreSQL DOUBLE PRECISION; DuckDB DOUBLE; constrained to 0 through 1 when presentProvider confidence score when available.
confidence_band_lower_atSQLite TEXT; PostgreSQL/DuckDB TIMESTAMPTZLower timestamp bound for provider uncertainty when available.
confidence_band_upper_atSQLite TEXT; PostgreSQL/DuckDB TIMESTAMPTZUpper timestamp bound for provider uncertainty when available.
data_freshness_atSQLite TEXT; PostgreSQL/DuckDB TIMESTAMPTZProvider freshness timestamp exposed through the ETA result contract.
source_feed_captured_atSQLite TEXT; PostgreSQL/DuckDB TIMESTAMPTZCapture time of the source feed used by this provider result.
metadata_jsonSQLite TEXT; PostgreSQL JSONB; DuckDB JSONProvider-specific metadata serialized as JSON.
declinedSQLite INTEGER checked 0/1; PostgreSQL/DuckDB BOOLEANWhether the provider intentionally declined to return an ETA.
staleSQLite INTEGER checked 0/1; PostgreSQL/DuckDB BOOLEANWhether the provider result was based on stale input.

static_gtfs_metadata

Source: DuckDB SCHEMA_SQL in src/marvia/store.py; this table is not created by the ADBC hot store.

Column Real type Meaning
idBIGINT PRIMARY KEY DEFAULT nextval(...)Persisted metadata row id.
fetched_atTIMESTAMPTZ NOT NULLUTC time when the static GTFS archive was fetched.
source_urlVARCHAR NOT NULLRenfe static GTFS zip URL.
etagVARCHARETag returned by Renfe for cache validation when present.
last_modifiedVARCHARLast-Modified header returned by Renfe when present.
zip_sha256VARCHAR NOT NULLSHA-256 digest of the downloaded GTFS zip.
zip_size_bytesUBIGINT NOT NULLSize of the downloaded GTFS zip in bytes.
manifest_jsonJSON NOT NULLStatic GTFS refresh manifest persisted as JSON.
stored_pathVARCHARLocal path where the materialized static GTFS snapshot was stored.
recorded_atTIMESTAMPTZ NOT NULL DEFAULT now()Database-side time when the metadata row was recorded.

hot_store_flush_watermarks

Source: ADBC hot-store SQL in src/marvia/store/hot.py; this table tracks cold-path flush progress.

Column Real type(s) Meaning
cursor_nameSQLite/PostgreSQL TEXT PRIMARY KEYNamed flush cursor, for example the cold-path Iceberg cursor.
last_row_idSQLite INTEGER NOT NULL DEFAULT 0; PostgreSQL BIGINT NOT NULL DEFAULT 0Highest hot-store raw_responses_realtime.id durably consumed by the cursor.
last_captured_atSQLite TEXT; PostgreSQL TIMESTAMPTZCapture timestamp for the last flushed realtime row.
updated_atSQLite TEXT NOT NULL; PostgreSQL TIMESTAMPTZ NOT NULLTime when the flush watermark was last updated.

Storage And Processing Technologies

Only technologies created or configured by the code are listed here; each entry points to the concrete module that proves it exists in the running architecture.

SQLite and PostgreSQL hot store through ADBC

The typed config selects sqlite locally and postgres for deployed profiles, and hot_store_from_config constructs SQLiteADBCHotStore or PostgresADBCHotStore using ADBC drivers. Evidence: src/marvia/config.py and src/marvia/store/hot.py.

DuckDB operational store and Iceberg query engine

The historical Storage class initializes DuckDB tables from SCHEMA_SQL. The cold read adapter installs and loads DuckDB's iceberg extension and reads table metadata through iceberg_scan(...). Evidence: src/marvia/store.py and src/marvia/query.py.

Iceberg warehouse with Parquet data files

load_or_create_realtime_iceberg_table creates the realtime warehouse table with an Iceberg schema and partition spec, while iceberg_table_properties sets write.format.default to parquet. Evidence: src/marvia/warehouse.py.

PyIceberg REST catalog

The config type is restricted to rest, and the catalog factory builds pyiceberg.catalog.rest.RestCatalog with configured URI and warehouse values. Evidence: src/marvia/config.py and src/marvia/catalog.py.

PyArrow FileIO over local files or S3-compatible storage

The warehouse FileIO helper always sets py-io-impl to pyiceberg.io.pyarrow.PyArrowFileIO, creates local file URIs for the local profile, and configures S3 endpoint properties when requested. Evidence: src/marvia/warehouse.py and src/marvia/config.py.

GTFS-Realtime and static GTFS ingestion

The poller configures Renfe's trip_updates.json, vehicle_positions.json, and alerts.json feeds with httpx; the static GTFS refresher downloads fomento_transit.zip and records metadata. Evidence: src/marvia/poller.py and src/marvia/static_gtfs.py.

Medallion arrivals_board precompute

compute_arrivals_board materializes parent board rows and provider result rows from immutable realtime/static snapshots before the hot store publishes them. Evidence: src/marvia/store/arrivals_board.py and src/marvia/store/hot.py.

Optional Spark Iceberg query engine

The query backend accepts spark, builds a Spark SQL Iceberg REST catalog configuration, and can wrap an existing Spark session behind the same read interface as DuckDB. Evidence: src/marvia/config.py and src/marvia/query.py.

ETA Providers

Broadcast is the displayed live provider column for live candidate trains. Physics runs as an internal baseline unless the operator includes it in the station provider display allow-list. Static GTFS is a fallback answer only when the station route cannot produce a live ETA row.

ETA provider sources and precedence Station requests discover live candidates from trip updates. Broadcast estimates use trip updates. The internal physics baseline uses vehicle positions and static shapes but is not rendered as a station column. Static schedule rows are used after live providers fail to produce rows. station request target stop_id and time live candidate discovery latest trip_updates stopTimeUpdate[] plus static trip/route metadata broadcast provider canonical_arrival from Renfe trip_updates stopTimeUpdate internal baseline recent vehicle_positions projected on static shape schedule fallback static GTFS departures used when no live ETA row exists 1 Live candidate row wins first If any provider returns an ETA, the row is shown with every visible provider column. 2 Display keeps broadcast visible Internal baselines can run without adding station columns. 3 Then schedule-only rows The station route falls back after live/provider rows are empty. Current static station page and map snapshot render live provider rows; the schedule fallback is implemented in the station route read path.

C1/C2 Network

The schematic follows the configured Malaga Cercanias station order: C1 to Fuengirola and C2 to Álora share the city trunk through Victoria Kent, then branch.

Cercanias Malaga C1 and C2 stop schematic C1 and C2 share Malaga-Centro Alameda, Malaga Maria Zambrano Cercanias, and Victoria Kent. C1 continues through the airport corridor to Fuengirola. C2 continues through Los Prados, Campanillas, Cartama, Aljaima, Pizarra, and Alora. C1 Fuengirola branch C2 Álora branch Málaga-Centro Alameda Málaga M.Z. Cercanías Victoria Kent Guadalhorce Málaga-Aeropuerto Plaza Mayor Los Álamos La Colina Torremolinos Montemar Alto El Pinillo Benalmádena-Arroyo Torremuelle Carvajal Torreblanca del Sol Los Boliches Fuengirola Málaga-Los Prados Campanillas Cártama Aljaima Pizarra Álora

These diagrams are inline SVG embedded in this page. They do not use Mermaid, JavaScript rendering, or an external CDN.