Jason Hunter 3 лет назад
Родитель
Сommit
ca20c735f7

+ 7 - 16
frigate/http.py

@@ -468,26 +468,17 @@ def recordings(camera_name):
         date = f"{search.group(1)}-{search.group(2)}"
         if date not in dates:
             dates[date] = OrderedDict()
-        dates[date][search.group(3)] = 0
+        dates[date][search.group(3)] = []
 
-    events = (
-        Event.select(
-            fn.DATE(Event.start_time, "unixepoch", "localtime"),
-            fn.STRFTIME("%H", Event.start_time, "unixepoch", "localtime"),
-            fn.COUNT(Event.id),
-        )
-        .where(Event.camera == camera_name)
-        .group_by(
-            fn.DATE(Event.start_time, "unixepoch", "localtime"),
-            fn.STRFTIME("%H", Event.start_time, "unixepoch", "localtime"),
-        )
-        .tuples()
-    )
+    events = Event.select().where(Event.camera == camera_name)
 
-    for date, hour, count in events:
+    e: Event
+    for e in events:
+        date = datetime.fromtimestamp(e.start_time)
         key = date.strftime("%Y-%m-%d")
+        hour = date.strftime("%H")
         if key in dates and hour in dates[key]:
-            dates[key][hour] = count
+            dates[key][hour].append(model_to_dict(e, exclude=[Event.thumbnail]))
 
     return jsonify(
         [

+ 1 - 1
web/src/App.jsx

@@ -29,7 +29,7 @@ export default function App() {
                   <AsyncRoute path="/cameras/:camera" getComponent={Routes.getCamera} />
                   <AsyncRoute path="/events/:eventId" getComponent={Routes.getEvent} />
                   <AsyncRoute path="/events" getComponent={Routes.getEvents} />
-                  <AsyncRoute path="/recordings/:camera/:date?/:hour?" getComponent={Routes.getRecording} />
+                  <AsyncRoute path="/recordings/:camera/:date?/:hour?/:seconds?" getComponent={Routes.getRecording} />
                   <AsyncRoute path="/debug" getComponent={Routes.getDebug} />
                   <AsyncRoute path="/styleguide" getComponent={Routes.getStyleGuide} />
                   <Cameras default path="/" />

+ 38 - 0
web/src/components/EventCard.jsx

@@ -0,0 +1,38 @@
+import { h } from 'preact';
+import { differenceInSeconds, fromUnixTime, format, startOfHour } from 'date-fns';
+import Link from '../components/Link';
+import { useApiHost } from '../api';
+
+export default function EventCard({ camera, event }) {
+  const apiHost = useApiHost();
+  const start = fromUnixTime(event.start_time);
+  const end = fromUnixTime(event.end_time);
+  const seconds = Math.max(differenceInSeconds(start, startOfHour(start)) - 10, 0);
+  return (
+    <Link className="" href={`/recordings/${camera}/${format(start, 'yyyy-MM-dd')}/${format(start, 'HH')}/${seconds}`}>
+      <div className="rounded-lg shadow-lg bg-gray-600 w-full flex flex-row flex-wrap p-3 antialiased mb-2">
+        <div className="w-1/2 md:w-1/3">
+          <img className="rounded-lg shadow-lg antialiased" src={`${apiHost}/api/events/${event.id}/thumbnail.jpg`} />
+        </div>
+        <div className="w-1/2 md:w-2/3 px-3 flex flex-row flex-wrap">
+          <div className="w-full text-right text-gray-700 font-semibold relative pt-0">
+            <div className="text-2xl text-white leading-tight capitalize">{event.label}</div>
+            <div className="text-lg text-white leading-tight">{(event.top_score * 100).toFixed(1)}%</div>
+            <div className="text-xs md:text-normal text-gray-300 hover:text-gray-400 cursor-pointer">
+              <span className="border-b border-dashed border-gray-500 pb-1">
+                {format(start, 'HH:mm:ss')} - {format(end, 'HH:mm:ss')}
+              </span>
+            </div>
+          </div>
+        </div>
+        <div className="hidden md:block w-full text-right">
+          <div className="text-sm text-gray-300 hover:text-gray-400 cursor-pointer md:absolute pt-3 md:pt-0 bottom-0 right-0">
+            {event.zones.map((zone) => (
+              <div>{zone}</div>
+            ))}
+          </div>
+        </div>
+      </div>
+    </Link>
+  );
+}

+ 12 - 6
web/src/components/RecordingPlaylist.jsx

@@ -2,6 +2,7 @@ import { h } from 'preact';
 import { useState } from 'preact/hooks';
 import { format, parseISO } from 'date-fns';
 import Accordion from '../components/Accordion';
+import EventCard from '../components/EventCard';
 import Link from '../components/Link';
 import Menu from '../icons/Menu';
 import MenuOpen from '../icons/MenuOpen';
@@ -16,11 +17,16 @@ export default function RecordingPlaylist({ camera, recordings, selectedDate })
     result.push(
       <Accordion title={format(date, 'MMM d, yyyy')} selected={recording.date === selectedDate}>
         {recording.recordings.map((item) => (
-          <div className="text-white bg-black bg-opacity-50 border-b border-gray-500 py-2 px-4 mb-1">
-            <Link href={`/recordings/${camera}/${recording.date}/${item.hour}`} type="text">
-              {item.hour}:00
-            </Link>
-            <span className="float-right">{item.events} Events</span>
+          <div className="mb-2">
+            <div className="text-white bg-black bg-opacity-50 border-b border-gray-500 py-2 px-4 mb-1">
+              <Link href={`/recordings/${camera}/${recording.date}/${item.hour}`} type="text">
+                {item.hour}:00
+              </Link>
+              <span className="float-right">{item.events.length} Events</span>
+            </div>
+            {item.events.map((event) => (
+              <EventCard camera={camera} event={event} />
+            ))}
           </div>
         ))}
       </Accordion>
@@ -30,7 +36,7 @@ export default function RecordingPlaylist({ camera, recordings, selectedDate })
   const openClass = active ? '-left-6' : 'right-0';
 
   return (
-    <div className="flex absolute inset-y-0 right-0 w-1/2 md:w-1/3 max-w-xl min-w-lg text-base text-white font-sans">
+    <div className="flex absolute inset-y-0 right-0 w-9/12 md:w-1/3 max-w-xl min-w-lg text-base text-white font-sans">
       <div
         onClick={toggle}
         className={`absolute ${openClass} cursor-pointer items-center self-center rounded-tl-lg rounded-bl-lg border border-r-0 w-6 h-20 py-7 bg-gray-800 bg-opacity-70`}

+ 7 - 1
web/src/routes/Recording.jsx

@@ -6,7 +6,7 @@ import RecordingPlaylist from '../components/RecordingPlaylist';
 import VideoPlayer from '../components/VideoPlayer';
 import { FetchStatus, useApiHost, useRecording } from '../api';
 
-export default function Recording({ camera, date, hour }) {
+export default function Recording({ camera, date, hour, seconds }) {
   const apiHost = useApiHost();
   const { data, status } = useRecording(camera);
 
@@ -50,6 +50,9 @@ export default function Recording({ camera, date, hour }) {
     this.player.playlist.autoadvance(0);
     if (selectedHour !== -1) {
       this.player.playlist.currentItem(selectedHour);
+      if (seconds !== undefined) {
+        this.player.currentTime(seconds);
+      }
     }
   }
 
@@ -64,6 +67,9 @@ export default function Recording({ camera, date, hour }) {
             player.playlist.autoadvance(0);
             if (selectedHour !== -1) {
               player.playlist.currentItem(selectedHour);
+              if (seconds !== undefined) {
+                player.currentTime(seconds);
+              }
             }
             this.player = player;
           }