diff --git a/main.py b/main.py
index 0f218c3..83f213a 100755
--- a/main.py
+++ b/main.py
@@ -2,44 +2,60 @@
 """Check prices of ferries to France."""
 
 import inspect
-from datetime import date, datetime
+import json
+import os
+import os.path
+import re
+from datetime import date, datetime, timedelta
 from typing import Any
 
-import configparser
-
 import flask
-import requests
+import pytz
+import routes
 import werkzeug.exceptions
 from werkzeug.debug.tbtools import get_current_traceback
+from werkzeug.wrappers import Response
 
-import pytz
+from ferry import ports
+from ferry.api import get_accommodations, get_prices
+from ferry.read_config import ferry_config, vehicle_from_config
 
 app = flask.Flask(__name__)
-app.debug = True
-
-ports = {
-    "PORTSMOUTH": "GBPME",
-    "PLYMOUTH": "GBPLY",
-    "POOLE": "GBPOO",
-    "CAEN": "FROUI",
-    "CHERBOURG": "FRCER",
-    "ST MALO": "FRSML",
-}
+app.debug = False
 
 
-ferry_config = configparser.ConfigParser()
-ferry_config.read("/home/edward/.config/brittany-ferries/config")
+def cache_location() -> str:
+    return os.path.expanduser(ferry_config.get("cache", "location"))
 
 
-def get_vehicle() -> dict[str, str | int | list[str] | dict[str, None]]:
-    """Return vehicle detail in the format for the Brittany Ferries API."""
-    return {
-        "type": ferry_config.get("vehicle", "type"),
-        "registrations": [ferry_config.get("vehicle", "registration")],
-        "height": ferry_config.getint("vehicle", "height"),
-        "length": ferry_config.getint("vehicle", "length"),
-        "extras": {"rearMountedBikeCarrier": None},
-    }
+def cache_filename(params: str) -> str:
+    """Get a filename to use for caching."""
+    now_str = datetime.utcnow().strftime("%Y-%m-%d_%H%M")
+    params = params.replace(":", "_").replace(".", "_")
+
+    return os.path.join(cache_location(), now_str + "_" + params + ".json")
+
+
+def time_to_minutes(t: str) -> int:
+    """Convert time (22:50) into minutes since midnight."""
+    m = re.match(r"^(\d\d):(\d\d)$", t)
+    assert m
+    hours, minutes = [int(j) for j in m.groups()]
+
+    return (60 * hours) + minutes
+
+
+def get_duration(depart: str, arrive: str, time_delta: int) -> str:
+    """Given two times calculate the duration and return as string."""
+    depart_min = time_to_minutes(depart)
+    arrive_min = time_to_minutes(arrive) + time_delta
+
+    duration = arrive_min - depart_min
+
+    if depart_min > arrive_min:
+        duration += 60 * 24
+
+    return f"{duration // 60}h{ duration % 60:02d}m"
 
 
 @app.errorhandler(werkzeug.exceptions.InternalServerError)
@@ -59,56 +75,10 @@ def exception_handler(e):
 
 
 def parse_date(d: str) -> date:
-    """Parse an ISO date."""
+    """Parse a date from a string in ISO format."""
     return datetime.strptime(d, "%Y-%m-%d").date()
 
 
-def get_accommodations(
-    departure_port: str, arrival_port: str, departure_date: str, ticket_tier: str
-):
-    url = "https://www.brittany-ferries.co.uk/api/ferry/v1/crossing/accommodations"
-    post_data = {
-        "bookingReference": None,
-        "departurePort": departure_port,
-        "arrivalPort": arrival_port,
-        "departureDate": departure_date,
-        "passengers": {"adults": 2, "children": 0, "infants": 0},
-        "disability": None,
-        "vehicle": get_vehicle(),
-        "petCabinsNeeded": False,
-        "ticketTier": ticket_tier,
-        "pets": {"smallDogs": 0, "largeDogs": 0, "cats": 0},
-        "sponsor": None,
-    }
-
-    r = requests.post(url, json=post_data)
-    return r.json()
-
-
-def get_prices(
-    departure_port: str, arrival_port: str, from_date: str, to_date: str
-) -> dict[str, Any]:
-    """Call Brittany Ferries API to get details of crossings."""
-    url = "https://www.brittany-ferries.co.uk/api/ferry/v1/crossing/prices"
-
-    post_data = {
-        "bookingReference": None,
-        "pets": {"smallDogs": 0, "largeDogs": 0, "cats": 0},
-        "passengers": {"adults": 2, "children": 0, "infants": 0},
-        "vehicle": get_vehicle(),
-        "departurePort": departure_port,
-        "arrivalPort": arrival_port,
-        "disability": None,
-        "sponsor": None,
-        "fromDate": f"{from_date}T00:00:00",
-        "toDate": f"{to_date}T23:59:59",
-    }
-
-    r = requests.post(url, json=post_data)
-    data: dict[str, Any] = r.json()
-    return data
-
-
 @app.route("/route/<departure_port>/<arrival_port>/<from_date>/<to_date>")
 def show_route(
     departure_port: str, arrival_port: str, from_date: str, to_date: str
@@ -128,8 +98,9 @@ def show_route(
 
 
 @app.route("/")
-def start():
+def start() -> Response:
     """Start page."""
+    return flask.render_template("index.html")
     return flask.redirect(flask.url_for("outbound_page"))
 
 
@@ -146,81 +117,183 @@ def cabins_url(dep, arr, crossing, ticket_tier):
     )
 
 
-def get_days_until_start():
+def get_days_until_start() -> int:
+    """How long until the travel date."""
     start = date.fromisoformat(ferry_config.get("dates", "start"))
     return (start - date.today()).days
 
 
-@app.route("/outbound")
-def outbound_page() -> str:
-    """Show all routes on one page."""
-    selection = [
-        ["PORTSMOUTH", "CAEN"],
-        ["PORTSMOUTH", "CHERBOURG"],
-        ["PORTSMOUTH", "ST MALO"],
-        ["POOLE", "CHERBOURG"],
-    ]
+def get_prices_with_cache(
+    name: str,
+    start: str,
+    end: str,
+    selection: list[tuple[str, str]],
+    refresh: bool = False,
+) -> list[tuple[str, str, dict[str, Any]]]:
 
-    from_date = ferry_config.get("outbound", "from")
-    to_date = ferry_config.get("outbound", "to")
+    params = f"{name}_{start}_{end}"
+    existing_files = os.listdir(cache_location())
+    existing = [f for f in existing_files if f.endswith(params + ".json")]
+    if not refresh and existing:
+        recent_filename = max(existing)
+        recent = datetime.strptime(recent_filename, f"%Y-%m-%d_%H%M_{params}.json")
+
+        now = datetime.utcnow()
+
+        delta = now - recent
+        if delta < timedelta(hours=1):
+            full = os.path.join(cache_location(), recent_filename)
+            data = json.load(open(full))
+            return data
+
+    vehicle = vehicle_from_config(ferry_config)
+    filename = cache_filename(params)
 
     all_data = [
-        (dep, arr, get_prices(ports[dep], ports[arr], from_date, to_date)["crossings"])
+        (dep, arr, get_prices(ports[dep], ports[arr], start, end, vehicle)["crossings"])
         for dep, arr in selection
     ]
+
+    with open(filename, "w") as out:
+        print(filename)
+        json.dump(all_data, out, indent=2)
+
+    return all_data
+
+
+def build_outbound(section: str) -> str:
+    """Show all routes on one page."""
+    selection = [
+        ("PORTSMOUTH", "CAEN"),
+        ("PORTSMOUTH", "CHERBOURG"),
+        ("PORTSMOUTH", "ST MALO"),
+        # ("POOLE", "CHERBOURG"),
+    ]
+
+    start = ferry_config.get(section, "from")
+    end = ferry_config.get(section, "to")
+    refresh = bool(flask.request.args.get("refresh"))
+
+    all_data = get_prices_with_cache(section, start, end, selection, refresh)
+
     return flask.render_template(
         "all_routes.html",
         data=all_data,
         days_until_start=get_days_until_start(),
         ports=ports,
         parse_date=parse_date,
-        from_date=from_date,
-        to_date=to_date,
-        other="return",
+        from_date=start,
+        to_date=end,
         cabins_url=cabins_url,
+        get_duration=get_duration,
+        time_delta=-60,
+        format_pet_options=format_pet_options,
     )
 
 
+@app.route("/outbound1")
+def outbound1_page() -> str:
+    return build_outbound("outbound1")
+
+
+@app.route("/outbound2")
+def outbound2_page() -> str:
+    return build_outbound("outbound2")
+
+
+def format_pet_options(o: dict[str, bool]) -> list[str]:
+    ret = []
+    if o.get("petCabinAvailable"):
+        ret.append("pet cabins")
+    if o.get("smallKennelAvailable"):
+        ret.append("small kennel")
+    if o.get("stayInCarAvailable"):
+        ret.append("pets stay in car")
+
+    return ret
+
+
 @app.route("/return")
 def return_page() -> str:
     """Show all routes on one page."""
     selection = [
-        ["CAEN", "PORTSMOUTH"],
-        ["CHERBOURG", "PORTSMOUTH"],
-        ["ST MALO", "PORTSMOUTH"],
-        ["CHERBOURG", "POOLE"],
+        ("CAEN", "PORTSMOUTH"),
+        ("CHERBOURG", "PORTSMOUTH"),
+        ("ST MALO", "PORTSMOUTH"),
+        # ("CHERBOURG", "POOLE"),
     ]
 
-    from_date = ferry_config.get("return", "from")
-    to_date = ferry_config.get("return", "to")
+    start = ferry_config.get("return", "from")
+    end = ferry_config.get("return", "to")
+    refresh = bool(flask.request.args.get("refresh"))
+
+    all_data = get_prices_with_cache("return", start, end, selection, refresh)
 
-    all_data = [
-        (dep, arr, get_prices(ports[dep], ports[arr], from_date, to_date)["crossings"])
-        for dep, arr in selection
-    ]
     return flask.render_template(
         "all_routes.html",
         data=all_data,
         ports=ports,
         days_until_start=get_days_until_start(),
         parse_date=parse_date,
-        from_date=from_date,
-        to_date=to_date,
-        other="outbound",
+        from_date=start,
+        to_date=end,
         cabins_url=cabins_url,
+        get_duration=get_duration,
+        time_delta=60,
+        format_pet_options=format_pet_options,
     )
 
 
+def get_accommodations_with_cache(
+    dep: str, arr: str, d: str, ticket_tier: str, refresh: bool = False
+) -> dict[str, list[dict[str, Any]]]:
+    params = f"{dep}_{arr}_{d}_{ticket_tier}"
+    existing_files = os.listdir(cache_location())
+    existing = [f for f in existing_files if f.endswith(params + ".json")]
+    if not refresh and existing:
+        recent_filename = max(existing)
+        recent = datetime.strptime(recent_filename, f"%Y-%m-%d_%H%M_{params}.json")
+
+        now = datetime.utcnow()
+
+        delta = now - recent
+        if delta < timedelta(hours=1):
+            full = os.path.join(cache_location(), recent_filename)
+            data = json.load(open(full))
+            return data
+
+    vehicle = vehicle_from_config(ferry_config)
+    filename = cache_filename(params)
+    data = get_accommodations(dep, arr, d, ticket_tier, vehicle)
+
+    with open(filename, "w") as out:
+        print(filename)
+        json.dump(data, out, indent=2)
+
+    return data
+
+
 @app.route("/cabins/<departure_port>/<arrival_port>/<departure_date>/<ticket_tier>")
-def cabins(departure_port, arrival_port, departure_date, ticket_tier):
-    data = get_accommodations(departure_port, arrival_port, departure_date, ticket_tier)
+def cabins(
+    departure_port: str, arrival_port: str, departure_date: str, ticket_tier: str
+) -> str:
+    cabin_data = get_accommodations_with_cache(
+        departure_port, arrival_port, departure_date, ticket_tier
+    )
+    accommodations = [
+        a
+        for a in cabin_data["accommodations"]
+        if a["quantityAvailable"] > 0 and a["code"] != "RS"
+        # and "Inside" not in a["description"]
+    ]
     return flask.render_template(
         "cabins.html",
         departure_port=departure_port,
         arrival_port=arrival_port,
         departure_date=departure_date,
         ticket_tier=ticket_tier,
-        accommodations=data["accommodations"],
+        accommodations=accommodations,
+        pet_accommodations=cabin_data["petAccommodations"],
     )
 
 
diff --git a/templates/all_routes.html b/templates/all_routes.html
index 0fabcdf..667cdc5 100644
--- a/templates/all_routes.html
+++ b/templates/all_routes.html
@@ -5,6 +5,24 @@
   <title>Ferries to France</title>
   <meta name="viewport" content="width=device-width, initial-scale=1">
   <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous">
+
+<!--
+<style>
+body {
+  background: black;
+  color: white;
+}
+
+  .table  { color: white; }
+
+a:link {
+  color: rgb(127, 127, 255);
+}
+
+</style>
+-->
+
+
 </head>
 
 {% from "individual_route.html" import route_table with context %}
@@ -12,9 +30,11 @@
 <body>
   <div class="m-3">
 
-    <p>{{ days_until_start }} days until start of Dodainville week: Friday 16 September 2022</p>
+    <p>{{ days_until_start }} days / {{ (days_until_start / 7) | int }} weeks / {{ "{:.1f}".format(days_until_start / 30.5) }} months until start of Dodainville week: Friday 17 March 2022</p>
 
+    {#
     <p><a href="{{ url_for(other + "_page") }}">{{ other }}</a></p>
+    #}
 
     {% if extra_routes %}
     <ul>
@@ -28,6 +48,10 @@
     <h4>{{ dep.title() }} to {{ arr.title() }}</h4>
       {{ route_table(dep, arr, days) }}
     {% endfor %}
+
+  <!--
+    {{ data | pprint | safe }}
+  -->
   </div>
 </body>
 </html>
diff --git a/templates/base.html b/templates/base.html
new file mode 100644
index 0000000..9d3c9ad
--- /dev/null
+++ b/templates/base.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+
+  <title>
+    {% block title %}{% endblock %}
+  </title>
+
+<style>
+/* body {
+  background: black;
+  color: white;
+} */
+</style>
+
+  {% block style %}{% endblock %}
+</head>
+
+<body>
+  {% block content %}{% endblock %}
+
+  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa" crossorigin="anonymous"></script>
+
+  {% block script %}{% endblock %}
+</body>
+</html>
diff --git a/templates/cabins.html b/templates/cabins.html
index 0720f22..9254ff6 100644
--- a/templates/cabins.html
+++ b/templates/cabins.html
@@ -5,6 +5,25 @@
   <title>Ferries to France</title>
   <meta name="viewport" content="width=device-width, initial-scale=1">
   <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous">
+
+  <!--
+<style>
+body {
+  background: black;
+  color: white;
+}
+
+  .table  { color: white; }
+
+a:link {
+  color: rgb(127, 127, 255);
+}
+
+</style>
+-->
+
+
+
 </head>
 
 <body>
@@ -14,12 +33,12 @@
 
     <p>{{ departure_date }} {{ ticket_tier }}</p>
 
-    <table class="table">
+    <table class="table w-auto">
       <tr>
         <th>code</th>
         <th>description</th>
-        <th>max adults</th>
-        <th>quantity available</th>
+        <th>births</th>
+        <th>quantity<br/>available</th>
         <th>price</th>
       </tr>
       {% for a in accommodations if a.quantityAvailable > 0 %}
@@ -27,12 +46,28 @@
         <td>{{ a.code }}</td>
         <td>{{ a.description }}</td>
         <td>{{ a.maxAdults }}</td>
-        <td>{{ a.quantityAvailable }}</td>
+        <td>
+          {% if a.quantityAvailable == 10 %}
+            10+
+          {% else %}
+            {{ a.quantityAvailable }}
+          {% endif %}
+        </td>
         <td>£{{ a.unitCost.amount }}</td>
       </tr>
     {% endfor %}
     </table>
 
+    <h4>Pet accommodations</h4>
+
+    <p>
+    o.petStayInCar = 'G',<br>
+    o.petLargeKennel = 'B',<br>
+    o.petSmallKennel = 'K',<br>
+    </p>
+
+    <pre>{{ pet_accommodations | tojson }}</pre>
+
   </div>
 </body>
 </html>
diff --git a/templates/index.html b/templates/index.html
index f8b87c6..93ab256 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -11,9 +11,9 @@
   <div class="m-3">
 
   <ul>
-    {% for dep, arr in routes %}
-    <li><a href="{{ url_for("show_route", departure_port=ports[dep], arrival_port=ports[arr]) }}">{{ dep }} - {{ arr }}</a></li>
-    {% endfor %}
+    <li><a href="{{ url_for("outbound1_page") }}">Outbound: 10 March</a>
+    <li><a href="{{ url_for("outbound2_page") }}">Outbound: 17 March</a>
+    <li><a href="{{ url_for("return_page") }}">Return: 24 March</a>
   </ul>
   </div>
 
diff --git a/templates/individual_route.html b/templates/individual_route.html
index 29a2002..13248eb 100644
--- a/templates/individual_route.html
+++ b/templates/individual_route.html
@@ -1,8 +1,10 @@
 {% macro headings() %}
     <tr>
+      <th>ID</th>
       <th>day</th>
       <th>depart</th>
       <th>arrive</th>
+      <th>duration</th>
       <th>ship</th>
       <th>economy</th>
       <th>standard</th>
@@ -17,10 +19,12 @@
   {{ headings() }}
   {% for day in days %}
   {% set date = parse_date(day.date) %}
-    {% for crossing in day.prices %}
+    {% for i in day.prices if i.crossingPrices.sailingId not in ("385413", "384486", "386181", "386191", "388752", "385445", "384550") %}
+    {% set crossing = i.crossingPrices %}
 
       {# <tr><td colspan="7">{{ crossing }}</td></tr> #}
     <tr>
+      <td class="text-nowrap">{{ crossing.sailingId }}</td>
       <td class="text-nowrap text-end">{{ date.strftime("%a, %d %b") }}</td>
       <td class="text-nowrap">
         {{ crossing.departureDateTime.time }}
@@ -28,6 +32,9 @@
       <td class="text-nowrap">
       {{ crossing.arrivalDateTime.time }}
       </td>
+      <td class="text-nowrap">
+        {{ get_duration(crossing.departureDateTime.time, crossing.arrivalDateTime.time, time_delta) }}
+      </td>
       <td class="text-nowrap">
       {{ crossing.shipName }}
       </td>
@@ -46,6 +53,9 @@
       £{{ crossing.flexiPrice.amount }}
         </a>
       </td>
+      <td>
+        {{ format_pet_options(crossing.petAvailabilities) | join(", ") }}
+      </td>
       <td class="text-nowrap">
       {% if crossing.full %}full |{% endif %}
       {% if crossing.isCabinSpaceFull %}no cabin space |{% endif %}
diff --git a/templates/show_error.html b/templates/show_error.html
index e75e75e..8d97299 100644
--- a/templates/show_error.html
+++ b/templates/show_error.html
@@ -18,8 +18,6 @@ URL: {{ request.url }}
 {{ tb.plaintext | safe }}
 {% endset %}
 
-<p><a class="btn btn-primary btn-lg" role="button" href="https://github.com/EdwardBetts/dab-mechanic/issues/new?title={{ tb.exception + " " + request.url | urlencode }}&body={{ body | urlencode }}">Submit as an issue on GitHub</a> (requires an account with GitHub)</p>
-
 <h2 class="traceback">Traceback <em>(most recent call last)</em></h2>
 {{ tb.render_summary(include_title=False) | safe }}