Implement downloading

This commit is contained in:
Nicolas Mowen 2024-04-19 10:30:39 -06:00
parent 2e36f6d257
commit 5657c9b5fe
3 changed files with 58 additions and 39 deletions

View File

@ -1166,9 +1166,13 @@ def preview_gif(camera_name: str, start_ts, end_ts, max_cache_age=2592000):
@MediaBp.route("/<camera_name>/start/<int:start_ts>/end/<int:end_ts>/preview.mp4") @MediaBp.route("/<camera_name>/start/<int:start_ts>/end/<int:end_ts>/preview.mp4")
@MediaBp.route("/<camera_name>/start/<float:start_ts>/end/<float:end_ts>/preview.mp4") @MediaBp.route("/<camera_name>/start/<float:start_ts>/end/<float:end_ts>/preview.mp4")
def preview_mp4(camera_name: str, start_ts, end_ts, max_cache_age=2592000): def preview_mp4(camera_name: str, start_ts, end_ts):
file_name = secure_filename(f"clip_{camera_name}_{start_ts}-{end_ts}.mp4")
path = os.path.join(CACHE_DIR, file_name)
if datetime.fromtimestamp(start_ts) < datetime.now().replace(minute=0, second=0): if datetime.fromtimestamp(start_ts) < datetime.now().replace(minute=0, second=0):
# has preview mp4 # has preview mp4
try:
preview: Previews = ( preview: Previews = (
Previews.select( Previews.select(
Previews.camera, Previews.camera,
@ -1186,6 +1190,8 @@ def preview_mp4(camera_name: str, start_ts, end_ts, max_cache_age=2592000):
.limit(1) .limit(1)
.get() .get()
) )
except DoesNotExist:
preview = None
if not preview: if not preview:
return make_response( return make_response(
@ -1200,6 +1206,7 @@ def preview_mp4(camera_name: str, start_ts, end_ts, max_cache_age=2592000):
"-hide_banner", "-hide_banner",
"-loglevel", "-loglevel",
"warning", "warning",
"-y",
"-ss", "-ss",
f"00:{minutes}:{seconds}", f"00:{minutes}:{seconds}",
"-t", "-t",
@ -1210,13 +1217,11 @@ def preview_mp4(camera_name: str, start_ts, end_ts, max_cache_age=2592000):
"8", "8",
"-vf", "-vf",
"setpts=0.12*PTS", "setpts=0.12*PTS",
"-loop",
"0",
"-c:v", "-c:v",
"copy", "libx264",
"-f", "-movflags",
"mp4", "+faststart",
"-", path,
] ]
process = sp.run( process = sp.run(
@ -1231,7 +1236,6 @@ def preview_mp4(camera_name: str, start_ts, end_ts, max_cache_age=2592000):
500, 500,
) )
gif_bytes = process.stdout
else: else:
# need to generate from existing images # need to generate from existing images
preview_dir = os.path.join(CACHE_DIR, "preview_frames") preview_dir = os.path.join(CACHE_DIR, "preview_frames")
@ -1275,13 +1279,11 @@ def preview_mp4(camera_name: str, start_ts, end_ts, max_cache_age=2592000):
"0", "0",
"-i", "-i",
"/dev/stdin", "/dev/stdin",
"-loop",
"0",
"-c:v", "-c:v",
"libx264", "libx264",
"-f", "-movflags",
"gif", "+faststart",
"-", path,
] ]
process = sp.run( process = sp.run(
@ -1297,11 +1299,14 @@ def preview_mp4(camera_name: str, start_ts, end_ts, max_cache_age=2592000):
500, 500,
) )
gif_bytes = process.stdout response = make_response()
response.headers["Content-Description"] = "File Transfer"
response = make_response(gif_bytes) response.headers["Cache-Control"] = "no-cache"
response.headers["Content-Type"] = "image/gif" response.headers["Content-Type"] = "video/mp4"
response.headers["Cache-Control"] = f"private, max-age={max_cache_age}" response.headers["Content-Length"] = os.path.getsize(path)
response.headers["X-Accel-Redirect"] = (
f"/cache/{file_name}" # nginx: https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_ignore_headers
)
return response return response

View File

@ -1,15 +1,17 @@
import ActivityIndicator from "../indicators/activity-indicator"; import ActivityIndicator from "../indicators/activity-indicator";
import { LuPencil, LuTrash } from "react-icons/lu"; import { LuTrash } from "react-icons/lu";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { useState } from "react"; import { useState } from "react";
import { isDesktop } from "react-device-detect"; import { isDesktop } from "react-device-detect";
import { FaPlay } from "react-icons/fa"; import { FaDownload, FaPlay } from "react-icons/fa";
import Chip from "../indicators/Chip"; import Chip from "../indicators/Chip";
import { Skeleton } from "../ui/skeleton"; import { Skeleton } from "../ui/skeleton";
import { Dialog, DialogContent, DialogFooter, DialogTitle } from "../ui/dialog"; import { Dialog, DialogContent, DialogFooter, DialogTitle } from "../ui/dialog";
import { Input } from "../ui/input"; import { Input } from "../ui/input";
import useKeyboardListener from "@/hooks/use-keyboard-listener"; import useKeyboardListener from "@/hooks/use-keyboard-listener";
import { Export } from "@/types/export"; import { Export } from "@/types/export";
import { MdEditSquare } from "react-icons/md";
import { baseUrl } from "@/api/baseUrl";
type ExportProps = { type ExportProps = {
className: string; className: string;
@ -114,13 +116,22 @@ export default function ExportCard({
<> <>
<div className="absolute inset-0 z-10 bg-black bg-opacity-60 rounded-2xl" /> <div className="absolute inset-0 z-10 bg-black bg-opacity-60 rounded-2xl" />
<div className="absolute top-1 right-1 flex items-center gap-2"> <div className="absolute top-1 right-1 flex items-center gap-2">
<a
className="z-20"
download
href={`${baseUrl}${exportedRecording.video_path.replace("/media/frigate/", "")}`}
>
<Chip className="bg-gradient-to-br from-gray-400 to-gray-500 bg-gray-500 rounded-md cursor-pointer">
<FaDownload className="size-4 text-white" />
</Chip>
</a>
<Chip <Chip
className="bg-gradient-to-br from-gray-400 to-gray-500 bg-gray-500 rounded-md cursor-pointer" className="bg-gradient-to-br from-gray-400 to-gray-500 bg-gray-500 rounded-md cursor-pointer"
onClick={() => onClick={() =>
setEditName({ original: exportedRecording.name, update: "" }) setEditName({ original: exportedRecording.name, update: "" })
} }
> >
<LuPencil className="size-4 text-white" /> <MdEditSquare className="size-4 text-white" />
</Chip> </Chip>
<Chip <Chip
className="bg-gradient-to-br from-gray-400 to-gray-500 bg-gray-500 rounded-md cursor-pointer" className="bg-gradient-to-br from-gray-400 to-gray-500 bg-gray-500 rounded-md cursor-pointer"
@ -161,7 +172,7 @@ export default function ExportCard({
)} )}
<div className="absolute bottom-0 inset-x-0 rounded-b-l z-10 h-[20%] bg-gradient-to-t from-black/60 to-transparent pointer-events-none rounded-2xl"> <div className="absolute bottom-0 inset-x-0 rounded-b-l z-10 h-[20%] bg-gradient-to-t from-black/60 to-transparent pointer-events-none rounded-2xl">
<div className="flex h-full justify-between items-end mx-3 pb-1 text-white text-sm capitalize"> <div className="flex h-full justify-between items-end mx-3 pb-1 text-white text-sm capitalize">
{exportedRecording.name} {exportedRecording.name.replaceAll("_", " ")}
</div> </div>
</div> </div>
</div> </div>

View File

@ -34,7 +34,10 @@ function Exports() {
} }
return exports.filter((exp) => return exports.filter((exp) =>
exp.name.toLowerCase().includes(search.toLowerCase()), exp.name
.toLowerCase()
.replaceAll("_", " ")
.includes(search.toLowerCase()),
); );
}, [exports, search]); }, [exports, search]);