diff --git a/src/clock.rs b/src/clock.rs
index 7c111f6..39b3509 100644
--- a/src/clock.rs
+++ b/src/clock.rs
@@ -1,22 +1,35 @@
 // Mostly nicked from https://github.com/icykoneko/ff14-fish-tracker-app/blob/master/js/app/time.js. Thanks!
 
-use chrono::{DateTime, Utc};
+use chrono::{DateTime, Timelike, Utc};
 
 const EARTH_TO_EORZEA: f64 = 3600. / 175.;
 const EORZEA_TO_EARTH: f64 = 1. / EARTH_TO_EORZEA;
 
+/// Converts a human timestamp to an Eorzean one.
 pub fn to_eorzea_time(earth_time: DateTime<Utc>) -> DateTime<Utc> {
     let et_ts = earth_time.timestamp();
     let new_ts = et_ts.abs() as f64 * EARTH_TO_EORZEA;
     DateTime::from_timestamp(new_ts.floor() as i64, 0).unwrap()
 }
 
+/// Gets the current date and time in Eorzea. Arguably you will never need the date, though,
+/// because it isn't shown anywhere in the game, the time is what matters.
 pub fn get_current_eorzea_date() -> DateTime<Utc> {
     to_eorzea_time(Utc::now())
 }
 
+/// Converts an Eorzean timestamp to human time.
 pub fn to_earth_time(ez_time: DateTime<Utc>) -> DateTime<Utc> {
     let ez_ts = ez_time.timestamp();
     let new_ts = (ez_ts.abs() as f64 * EORZEA_TO_EARTH).ceil();
     DateTime::from_timestamp(new_ts as i64, 0).unwrap()
 }
+
+/// Sets hours and minutes for a date, based on a f32.
+pub fn set_hm_from_float(date: &DateTime<Utc>, hm: f32) -> DateTime<Utc> {
+    let minutes = ((hm % 1.) * 100.) as u32;
+    date.date_naive()
+        .and_hms_opt(hm as u32, minutes, date.second())
+        .unwrap()
+        .and_utc()
+}
diff --git a/src/data.rs b/src/data.rs
index aba5924..cf62ecb 100644
--- a/src/data.rs
+++ b/src/data.rs
@@ -1,11 +1,13 @@
 use std::collections::HashMap;
 
-use chrono::Timelike;
+use chrono::{DateTime, Duration, Timelike, Utc};
 use serde::{Deserialize, Serialize};
 
 use crate::{
     clock,
-    forecast::{Forecast, ForecastSet, Rate},
+    forecast::{
+        self, modify_weather_time, round_to_last_weather_time, Forecast, ForecastSet, Rate,
+    },
 };
 
 const DATA: &'static str = include_str!("../data.json");
@@ -32,7 +34,7 @@ pub struct SubData {
     pub weather_types: HashMap<u32, WeatherType>,
 }
 
-#[derive(Serialize, Deserialize, Debug)]
+#[derive(Serialize, Deserialize, Debug, Default)]
 #[serde(rename_all(deserialize = "camelCase"))]
 pub struct FishEntry {
     #[serde(alias = "_id")]
@@ -83,7 +85,7 @@ pub struct Location {
     pub map_coords: (f32, f32, u32),
 }
 
-#[derive(Serialize, Deserialize, Debug)]
+#[derive(Serialize, Deserialize, Debug, Default)]
 pub struct FishMeta {
     pub id: u32,
     pub name_en: String,
@@ -92,14 +94,23 @@ pub struct FishMeta {
 pub struct CombinedFish<'a> {
     pub entry: &'a FishEntry,
     pub meta: &'a FishMeta,
+    pub is_up: bool,
+    pub next_uptime: DateTime<Utc>,
 }
 
 impl<'a> CombinedFish<'a> {
-    pub fn is_in_time_range(&self) -> bool {
+    /// Fills in the rest of the struct.
+    pub fn find_uptime(&mut self, data: &Data) {
+        self.is_up = self.is_up_now(data);
+        self.next_uptime = self.get_next_uptime(data);
+    }
+
+    /// Finds out whether the fish is technically up at the given time (discounting factors like weather).
+    pub fn is_in_time_range_at(&self, date: DateTime<Utc>) -> bool {
         if self.entry.start_hour.is_none() || self.entry.end_hour.is_none() {
             return false;
         }
-        let et = clock::get_current_eorzea_date();
+        let et = clock::to_eorzea_time(date);
         let start_hour = self.entry.start_hour.unwrap();
         let end_hour = self.entry.end_hour.unwrap();
         let spans_midnight = start_hour > end_hour;
@@ -111,6 +122,7 @@ impl<'a> CombinedFish<'a> {
         }
     }
 
+    /// Gets the associated forecast struct.
     pub fn get_forecast<'b>(&self, data: &'b Data) -> Option<&'b Forecast> {
         if self.entry.location.is_none() {
             return None;
@@ -124,12 +136,13 @@ impl<'a> CombinedFish<'a> {
         data.forecasts.get(&spot.forecast_id)
     }
 
-    pub fn is_in_correct_weather(&self, forecast: &Forecast) -> bool {
+    /// Finds out whether the fish is in favorable weather conditions at a specific Eorzean time.
+    pub fn is_in_correct_weather_at(&self, forecast: &Forecast, date: DateTime<Utc>) -> bool {
         if self.entry.weather_set.is_empty() {
             return true;
         }
         // Check if the current weather is right!
-        let cur_weather = forecast.weather_now();
+        let cur_weather = forecast.weather_at(date);
         let is_current = self
             .entry
             .weather_set
@@ -141,7 +154,8 @@ impl<'a> CombinedFish<'a> {
         let is_past = if self.entry.previous_weather_set.is_empty() {
             true
         } else {
-            let prev_weather = forecast.nth_weather(-1);
+            let prev_date = date - Duration::hours(8);
+            let prev_weather = forecast.weather_at(prev_date);
             self.entry
                 .previous_weather_set
                 .iter()
@@ -151,10 +165,71 @@ impl<'a> CombinedFish<'a> {
         is_current && is_past
     }
 
-    pub fn is_up(&self, data: &Data) -> bool {
+    /// Finds out if the fish is up now, includes weather and time bounds.
+    pub fn is_up_now(&self, data: &Data) -> bool {
+        self.is_up_at_n(data, 0)
+    }
+
+    /// Finds out if the fish is up at a specific time, `n` Eorzean hours away.
+    pub fn is_up_at_n(&self, data: &Data, n: i64) -> bool {
         let forecast = self.get_forecast(data);
-        self.is_in_time_range()
-            && forecast.is_none_or(|forecast| self.is_in_correct_weather(forecast))
+        let date = clock::get_current_eorzea_date() + Duration::hours(n);
+        self.is_in_time_range_at(date)
+            && forecast.is_none_or(|forecast| self.is_in_correct_weather_at(forecast, date))
+    }
+
+    /// Get the next uptime for a fish that is only restricted by time.
+    pub fn get_next_time_uptime(&self) -> DateTime<Utc> {
+        let ez_date = clock::get_current_eorzea_date();
+        // Check if we have a minute part to our start time, which can happen.
+        let adjusted_date = clock::set_hm_from_float(&ez_date, self.entry.start_hour.unwrap());
+        clock::to_earth_time(adjusted_date)
+    }
+
+    /// Get the next times when the weather is favorable for a fish.
+    pub fn get_next_weather_uptimes(&self, data: &Data, cycles: u32) -> Vec<DateTime<Utc>> {
+        let forecast = self.get_forecast(data).unwrap();
+        // Skip ahead by 8-hour intervals, to quickly find the next favorable weather.
+        let mut date = round_to_last_weather_time(clock::get_current_eorzea_date());
+        // Start with the next weather cycle.
+        date = date + Duration::hours(8);
+        let mut results = Vec::new();
+        for i in 1..cycles {
+            while !self.is_in_correct_weather_at(forecast, date) {
+                date = date + Duration::hours(8);
+            }
+            results.push(date.clone());
+            date = date + Duration::hours(8);
+        }
+        results
+    }
+
+    // Go until you find the next uptime slot, and return its date.
+    pub fn get_next_uptime(&self, data: &Data) -> DateTime<Utc> {
+        // Is this a time-only fish? If so, just return the next time.
+        if self.entry.weather_set.is_empty() && self.entry.start_hour.is_some() {
+            return clock::to_earth_time(self.get_next_time_uptime());
+        }
+
+        // If not, check if there's a weather-only fish
+        let next_weather_uptimes = self.get_next_weather_uptimes(data, 50);
+        if !self.entry.weather_set.is_empty() && self.entry.start_hour.is_none() {
+            return clock::to_earth_time(next_weather_uptimes.first().unwrap().clone());
+        }
+
+        // Now it gets a little bit tricky: We need to find a weather slot that also includes the start point for the
+        // time window.
+        for date in next_weather_uptimes {
+            // Does the start time lie within this window?
+            let uptime_end = date + Duration::hours(8);
+            let start_hour = self.entry.start_hour.unwrap();
+            if date.hour() as f32 <= start_hour && start_hour <= uptime_end.hour() as f32 {
+                return clock::to_earth_time(clock::set_hm_from_float(&date, start_hour));
+            }
+        }
+
+        // FIXME: This should never be called?
+        Utc::now()
     }
 }
 
@@ -192,7 +267,16 @@ impl Data {
             .iter()
             .filter_map(|(k, v)| {
                 let corresponding_meta = self.fish_entries.iter().find(|m| &m.id == k);
-                corresponding_meta.map(|m| (k.clone(), CombinedFish { entry: v, meta: m }))
+                corresponding_meta.map(|m| {
+                    let mut cfish = CombinedFish {
+                        entry: v,
+                        meta: m,
+                        is_up: false,            // fake default values for now
+                        next_uptime: Utc::now(), // dito
+                    };
+                    cfish.find_uptime(self);
+                    (k.clone(), cfish)
+                })
             })
             .collect()
     }
diff --git a/src/forecast.rs b/src/forecast.rs
index 70a6343..abf69f4 100644
--- a/src/forecast.rs
+++ b/src/forecast.rs
@@ -1,4 +1,4 @@
-use std::{collections::HashMap, f32::consts::PI};
+use std::collections::HashMap;
 
 use chrono::{DateTime, Duration, Timelike, Utc};
 
@@ -27,44 +27,51 @@ pub struct Rate {
 pub type ForecastSet = HashMap<u32, Forecast>;
 
 impl Forecast {
+    /// Calculates the current weather for the given target magic number.
     pub fn weather_for_target(&self, target: u32) -> &Rate {
         // TODO: Don't unwrap here!
         self.rates.iter().find(|r| target < r.rate).unwrap()
     }
 
+    /// Calculates the weather right now.
     pub fn weather_now(&self) -> &Rate {
         let utc = Utc::now();
         let target = calculate_target(utc);
         self.weather_for_target(target)
     }
 
-    /// Returns the weather `n` cycles before or after the current weather.
-    pub fn nth_weather(&self, n: i32) -> &Rate {
-        let last_weather_time = round_to_last_weather_time(&clock::get_current_eorzea_date());
-        let hours = n * 8;
-        let td = Duration::hours(hours.abs() as i64);
-        let new_date = if hours > 0 {
-            last_weather_time.checked_add_signed(td).unwrap()
-        } else {
-            last_weather_time.checked_sub_signed(td).unwrap()
-        };
-        let target = calculate_target(new_date);
+    /// Calculates the weather at a specific point in Eorzean time.
+    pub fn weather_at(&self, date: DateTime<Utc>) -> &Rate {
+        let new_date = round_to_last_weather_time(date);
+        let target = calculate_target(clock::to_earth_time(new_date));
         self.weather_for_target(target)
     }
 }
 
 /// Rounds to the last weather "start". These happen three times a day, at 0:00,
 /// at 8:00, and at 16:00.
-pub fn round_to_last_weather_time(date: &DateTime<Utc>) -> DateTime<Utc> {
+pub fn round_to_last_weather_time(date: DateTime<Utc>) -> DateTime<Utc> {
     let cur_hour = date.hour();
     // This essentially performs division without rest.
     let last_hour = (cur_hour / 8) * 8;
     date.date_naive()
-        .and_hms_opt(last_hour, date.minute(), date.second())
+        .and_hms_opt(last_hour, 0, 0)
         .unwrap()
         .and_utc()
 }
 
+/// Adds or subtracts `n` intervals of 8 hours to the last weather time.
+pub fn modify_weather_time(n: i32) -> DateTime<Utc> {
+    let last_weather_time = round_to_last_weather_time(clock::get_current_eorzea_date());
+    let hours = n * 8;
+    let td = Duration::hours(hours.abs() as i64);
+    if hours > 0 {
+        last_weather_time.checked_add_signed(td).unwrap()
+    } else {
+        last_weather_time.checked_sub_signed(td).unwrap()
+    }
+}
+
 /// Calculates the magic target number for a specific date.
 /// It's important to note that this expects a human time, not an Eorzean time.
 pub fn calculate_target(m: DateTime<Utc>) -> u32 {
diff --git a/src/main.rs b/src/main.rs
index 116ef03..eb67ab5 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -36,24 +36,16 @@ where
 #[axum::debug_handler]
 async fn main_handler(state: State<Arc<AppState>>) -> Result<Markup, AppError> {
     let meta = state.data.fish_with_meta();
-    let mut values: Vec<(&CombinedFish, bool)> = meta
-        .values()
-        .filter_map(|fish| {
-            if fish.entry.big_fish {
-                let is_up = fish.is_up(&state.data);
-                Some((fish, is_up))
-            } else {
-                None
-            }
-        })
-        .collect();
-    values.sort_by(|(_, aup), (_, bup)| bup.cmp(aup));
+    let mut values: Vec<&CombinedFish> = meta.values().filter(|f| f.entry.big_fish).collect();
+    values.sort_by(|afish, bfish| bfish.is_up.cmp(&afish.is_up));
     Ok(html! {
         h1 { "Hello! Current ET " (clock::get_current_eorzea_date().format("%H:%M")) }
-        @for (fish, is_up) in values {
+        @for fish in values {
             li {
-                @if is_up {
+                @if fish.is_up {
                     "Up! "
+                } @else {
+                   "Next uptime " (fish.next_uptime)
                 }
                 (fish.meta.name_en)
                 details {