Video Gifs #34
77
Cargo.lock
generated
77
Cargo.lock
generated
@@ -320,7 +320,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
|
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"getrandom",
|
"getrandom 0.2.15",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"version_check",
|
"version_check",
|
||||||
"zerocopy",
|
"zerocopy",
|
||||||
@@ -599,7 +599,7 @@ checksum = "2b1866ecef4f2d06a0bb77880015fdf2b89e25a1c2e5addacb87e459c86dc67e"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"blowfish",
|
"blowfish",
|
||||||
"getrandom",
|
"getrandom 0.2.15",
|
||||||
"subtle",
|
"subtle",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
@@ -1037,12 +1037,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "errno"
|
name = "errno"
|
||||||
version = "0.3.9"
|
version = "0.3.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
|
checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1214,10 +1214,22 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi",
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"r-efi",
|
||||||
|
"wasi 0.14.2+wasi-0.2.4",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gimli"
|
name = "gimli"
|
||||||
version = "0.31.1"
|
version = "0.31.1"
|
||||||
@@ -1632,6 +1644,7 @@ dependencies = [
|
|||||||
"rayon",
|
"rayon",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"tempfile",
|
||||||
"tokio",
|
"tokio",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
@@ -1802,9 +1815,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.164"
|
version = "0.2.173"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f"
|
checksum = "d8cfeafaffdbc32176b64fb251369d52ea9f0a8fbc6f8759edffef7b525d64bb"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libfuzzer-sys"
|
name = "libfuzzer-sys"
|
||||||
@@ -1839,9 +1852,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.4.14"
|
version = "0.9.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
|
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "litemap"
|
name = "litemap"
|
||||||
@@ -1974,7 +1987,7 @@ checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"wasi",
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1987,7 +2000,7 @@ dependencies = [
|
|||||||
"hermit-abi",
|
"hermit-abi",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"wasi",
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2432,6 +2445,12 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "r-efi"
|
||||||
|
version = "5.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.8.5"
|
version = "0.8.5"
|
||||||
@@ -2459,7 +2478,7 @@ version = "0.6.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom",
|
"getrandom 0.2.15",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2627,7 +2646,7 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"getrandom",
|
"getrandom 0.2.15",
|
||||||
"libc",
|
"libc",
|
||||||
"spin",
|
"spin",
|
||||||
"untrusted",
|
"untrusted",
|
||||||
@@ -2651,15 +2670,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "0.38.41"
|
version = "1.0.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6"
|
checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.6.0",
|
||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys",
|
"linux-raw-sys",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2917,12 +2936,12 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tempfile"
|
name = "tempfile"
|
||||||
version = "3.14.0"
|
version = "3.20.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c"
|
checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
|
||||||
"fastrand",
|
"fastrand",
|
||||||
|
"getrandom 0.3.3",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix",
|
"rustix",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
@@ -3324,6 +3343,15 @@ version = "0.11.0+wasi-snapshot-preview1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.14.2+wasi-0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
|
||||||
|
dependencies = [
|
||||||
|
"wit-bindgen-rt",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.95"
|
version = "0.2.95"
|
||||||
@@ -3606,6 +3634,15 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen-rt"
|
||||||
|
version = "0.39.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.6.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "write16"
|
name = "write16"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
|||||||
@@ -42,3 +42,4 @@ opentelemetry_sdk = { version = "0.28.0", features = ["default", "rt-tokio-curre
|
|||||||
opentelemetry-otlp = { version = "0.28.0", features = ["default", "metrics", "tracing", "grpc-tonic"] }
|
opentelemetry-otlp = { version = "0.28.0", features = ["default", "metrics", "tracing", "grpc-tonic"] }
|
||||||
opentelemetry-stdout = "0.28.0"
|
opentelemetry-stdout = "0.28.0"
|
||||||
opentelemetry-appender-log = "0.28.0"
|
opentelemetry-appender-log = "0.28.0"
|
||||||
|
tempfile = "3.20.0"
|
||||||
@@ -134,10 +134,20 @@ pub enum PhotoSize {
|
|||||||
Thumb,
|
Thumb,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct ThumbnailRequest {
|
pub struct ThumbnailRequest {
|
||||||
pub path: String,
|
pub(crate) path: String,
|
||||||
pub size: Option<PhotoSize>,
|
pub(crate)size: Option<PhotoSize>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(crate)format: Option<ThumbnailFormat>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, PartialEq)]
|
||||||
|
pub enum ThumbnailFormat {
|
||||||
|
#[serde(rename = "gif")]
|
||||||
|
Gif,
|
||||||
|
#[serde(rename = "image")]
|
||||||
|
Image,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
|||||||
64
src/files.rs
64
src/files.rs
@@ -24,7 +24,7 @@ use crate::data::SortType::NameAsc;
|
|||||||
use crate::error::IntoHttpError;
|
use crate::error::IntoHttpError;
|
||||||
use crate::otel::{extract_context_from_request, global_tracer};
|
use crate::otel::{extract_context_from_request, global_tracer};
|
||||||
use crate::tags::{FileWithTagCount, TagDao};
|
use crate::tags::{FileWithTagCount, TagDao};
|
||||||
use crate::video::StreamActor;
|
use crate::video::actors::StreamActor;
|
||||||
use path_absolutize::*;
|
use path_absolutize::*;
|
||||||
use rand::prelude::SliceRandom;
|
use rand::prelude::SliceRandom;
|
||||||
use rand::thread_rng;
|
use rand::thread_rng;
|
||||||
@@ -511,7 +511,6 @@ mod tests {
|
|||||||
use crate::{
|
use crate::{
|
||||||
data::{Claims, PhotosResponse},
|
data::{Claims, PhotosResponse},
|
||||||
testhelpers::BodyReader,
|
testhelpers::BodyReader,
|
||||||
video::StreamActor,
|
|
||||||
AppState,
|
AppState,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -519,6 +518,7 @@ mod tests {
|
|||||||
use crate::tags::SqliteTagDao;
|
use crate::tags::SqliteTagDao;
|
||||||
use actix_web::web::Data;
|
use actix_web::web::Data;
|
||||||
use std::{fs, sync::Arc};
|
use std::{fs, sync::Arc};
|
||||||
|
use actix_web::test::TestRequest;
|
||||||
|
|
||||||
fn setup() {
|
fn setup() {
|
||||||
let _ = env_logger::builder().is_test(true).try_init();
|
let _ = env_logger::builder().is_test(true).try_init();
|
||||||
@@ -547,13 +547,9 @@ mod tests {
|
|||||||
|
|
||||||
let response: HttpResponse = list_photos(
|
let response: HttpResponse = list_photos(
|
||||||
claims,
|
claims,
|
||||||
|
TestRequest::default().to_http_request(),
|
||||||
request,
|
request,
|
||||||
Data::new(AppState::new(
|
Data::new(AppState::test_state()),
|
||||||
Arc::new(StreamActor {}.start()),
|
|
||||||
String::from("/tmp"),
|
|
||||||
String::from("/tmp/thumbs"),
|
|
||||||
String::from("/tmp/video"),
|
|
||||||
)),
|
|
||||||
Data::new(RealFileSystem::new(String::from("/tmp"))),
|
Data::new(RealFileSystem::new(String::from("/tmp"))),
|
||||||
Data::new(Mutex::new(SqliteTagDao::default())),
|
Data::new(Mutex::new(SqliteTagDao::default())),
|
||||||
)
|
)
|
||||||
@@ -589,14 +585,9 @@ mod tests {
|
|||||||
|
|
||||||
let response = list_photos(
|
let response = list_photos(
|
||||||
claims,
|
claims,
|
||||||
HttpRequest::default(),
|
TestRequest::default().to_http_request(),
|
||||||
request,
|
request,
|
||||||
Data::new(AppState::new(
|
Data::new(AppState::test_state()),
|
||||||
Arc::new(StreamActor {}.start()),
|
|
||||||
String::from("/tmp"),
|
|
||||||
String::from("/tmp/thumbs"),
|
|
||||||
String::from("/tmp/video"),
|
|
||||||
)),
|
|
||||||
Data::new(RealFileSystem::new(String::from("./"))),
|
Data::new(RealFileSystem::new(String::from("./"))),
|
||||||
Data::new(Mutex::new(SqliteTagDao::default())),
|
Data::new(Mutex::new(SqliteTagDao::default())),
|
||||||
)
|
)
|
||||||
@@ -618,12 +609,12 @@ mod tests {
|
|||||||
|
|
||||||
let mut tag_dao = SqliteTagDao::new(in_memory_db_connection());
|
let mut tag_dao = SqliteTagDao::new(in_memory_db_connection());
|
||||||
|
|
||||||
let tag1 = tag_dao.create_tag("tag1").unwrap();
|
let tag1 = tag_dao.create_tag(&opentelemetry::Context::current(), "tag1").unwrap();
|
||||||
let _tag2 = tag_dao.create_tag("tag2").unwrap();
|
let _tag2 = tag_dao.create_tag(&opentelemetry::Context::current(), "tag2").unwrap();
|
||||||
let tag3 = tag_dao.create_tag("tag3").unwrap();
|
let tag3 = tag_dao.create_tag(&opentelemetry::Context::current(), "tag3").unwrap();
|
||||||
|
|
||||||
let _ = &tag_dao.tag_file("test.jpg", tag1.id).unwrap();
|
let _ = &tag_dao.tag_file(&opentelemetry::Context::current(), "test.jpg", tag1.id).unwrap();
|
||||||
let _ = &tag_dao.tag_file("test.jpg", tag3.id).unwrap();
|
let _ = &tag_dao.tag_file(&opentelemetry::Context::current(), "test.jpg", tag3.id).unwrap();
|
||||||
|
|
||||||
let mut files = HashMap::new();
|
let mut files = HashMap::new();
|
||||||
files.insert(
|
files.insert(
|
||||||
@@ -637,14 +628,9 @@ mod tests {
|
|||||||
|
|
||||||
let response: HttpResponse = list_photos(
|
let response: HttpResponse = list_photos(
|
||||||
claims,
|
claims,
|
||||||
HttpRequest::default(),
|
TestRequest::default().to_http_request(),
|
||||||
request,
|
request,
|
||||||
Data::new(AppState::new(
|
Data::new(AppState::test_state()),
|
||||||
Arc::new(StreamActor {}.start()),
|
|
||||||
String::from(""),
|
|
||||||
String::from("/tmp/thumbs"),
|
|
||||||
String::from("/tmp/video"),
|
|
||||||
)),
|
|
||||||
Data::new(FakeFileSystem::new(files)),
|
Data::new(FakeFileSystem::new(files)),
|
||||||
Data::new(Mutex::new(tag_dao)),
|
Data::new(Mutex::new(tag_dao)),
|
||||||
)
|
)
|
||||||
@@ -668,15 +654,15 @@ mod tests {
|
|||||||
|
|
||||||
let mut tag_dao = SqliteTagDao::new(in_memory_db_connection());
|
let mut tag_dao = SqliteTagDao::new(in_memory_db_connection());
|
||||||
|
|
||||||
let tag1 = tag_dao.create_tag("tag1").unwrap();
|
let tag1 = tag_dao.create_tag(&opentelemetry::Context::current(), "tag1").unwrap();
|
||||||
let _tag2 = tag_dao.create_tag("tag2").unwrap();
|
let _tag2 = tag_dao.create_tag(&opentelemetry::Context::current(), "tag2").unwrap();
|
||||||
let tag3 = tag_dao.create_tag("tag3").unwrap();
|
let tag3 = tag_dao.create_tag(&opentelemetry::Context::current(), "tag3").unwrap();
|
||||||
|
|
||||||
let _ = &tag_dao.tag_file("test.jpg", tag1.id).unwrap();
|
let _ = &tag_dao.tag_file(&opentelemetry::Context::current(), "test.jpg", tag1.id).unwrap();
|
||||||
let _ = &tag_dao.tag_file("test.jpg", tag3.id).unwrap();
|
let _ = &tag_dao.tag_file(&opentelemetry::Context::current(), "test.jpg", tag3.id).unwrap();
|
||||||
|
|
||||||
// Should get filtered since it doesn't have tag3
|
// Should get filtered since it doesn't have tag3
|
||||||
tag_dao.tag_file("some-other.jpg", tag1.id).unwrap();
|
tag_dao.tag_file(&opentelemetry::Context::current(), "some-other.jpg", tag1.id).unwrap();
|
||||||
|
|
||||||
let mut files = HashMap::new();
|
let mut files = HashMap::new();
|
||||||
files.insert(
|
files.insert(
|
||||||
@@ -696,13 +682,9 @@ mod tests {
|
|||||||
|
|
||||||
let response: HttpResponse = list_photos(
|
let response: HttpResponse = list_photos(
|
||||||
claims,
|
claims,
|
||||||
|
TestRequest::default().to_http_request(),
|
||||||
request,
|
request,
|
||||||
Data::new(AppState::new(
|
Data::new(AppState::test_state()),
|
||||||
Arc::new(StreamActor {}.start()),
|
|
||||||
String::from(""),
|
|
||||||
String::from("/tmp/thumbs"),
|
|
||||||
String::from("/tmp/video"),
|
|
||||||
)),
|
|
||||||
Data::new(FakeFileSystem::new(files)),
|
Data::new(FakeFileSystem::new(files)),
|
||||||
Data::new(Mutex::new(tag_dao)),
|
Data::new(Mutex::new(tag_dao)),
|
||||||
)
|
)
|
||||||
@@ -773,8 +755,8 @@ mod tests {
|
|||||||
assert!(is_valid_full_path(&base, &test_file, false).is_some());
|
assert!(is_valid_full_path(&base, &test_file, false).is_some());
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Some(PathBuf::from("/tmp/test.png")),
|
Some(PathBuf::from(test_file.clone())),
|
||||||
is_valid_full_path(&base, &PathBuf::from("/tmp/test.png"), false)
|
is_valid_full_path(&base, &PathBuf::from(test_file), false)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
41
src/main.rs
41
src/main.rs
@@ -42,7 +42,10 @@ use crate::otel::{extract_context_from_request, global_tracer};
|
|||||||
use crate::service::ServiceBuilder;
|
use crate::service::ServiceBuilder;
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
use crate::tags::*;
|
use crate::tags::*;
|
||||||
use crate::video::*;
|
use crate::video::actors::{
|
||||||
|
create_playlist, generate_video_thumbnail, ProcessMessage, ScanDirectoryMessage,
|
||||||
|
};
|
||||||
|
use crate::video::generate_video_gifs;
|
||||||
use log::{debug, error, info, trace, warn};
|
use log::{debug, error, info, trace, warn};
|
||||||
use opentelemetry::trace::{Span, Status, TraceContextExt, Tracer};
|
use opentelemetry::trace::{Span, Status, TraceContextExt, Tracer};
|
||||||
use opentelemetry::{global, KeyValue};
|
use opentelemetry::{global, KeyValue};
|
||||||
@@ -96,23 +99,29 @@ async fn get_image(
|
|||||||
.expect("Error stripping base path prefix from thumbnail");
|
.expect("Error stripping base path prefix from thumbnail");
|
||||||
|
|
||||||
let thumbs = &app_state.thumbnail_path;
|
let thumbs = &app_state.thumbnail_path;
|
||||||
let thumb_path = Path::new(&thumbs).join(relative_path);
|
let mut thumb_path = Path::new(&thumbs).join(relative_path);
|
||||||
|
|
||||||
|
// If it's a video and GIF format is requested, try to serve GIF thumbnail
|
||||||
|
if req.format == Some(ThumbnailFormat::Gif) && is_video_file(&path) {
|
||||||
|
thumb_path = Path::new(&app_state.gif_path).join(relative_path);
|
||||||
|
thumb_path.set_extension("gif");
|
||||||
|
}
|
||||||
|
|
||||||
trace!("Thumbnail path: {:?}", thumb_path);
|
trace!("Thumbnail path: {:?}", thumb_path);
|
||||||
if let Ok(file) = NamedFile::open(&thumb_path) {
|
if let Ok(file) = NamedFile::open(&thumb_path) {
|
||||||
span.set_status(Status::Ok);
|
span.set_status(Status::Ok);
|
||||||
file.into_response(&request)
|
// The NamedFile will automatically set the correct content-type
|
||||||
} else {
|
return file.into_response(&request);
|
||||||
span.set_status(Status::error("Not found"));
|
|
||||||
HttpResponse::NotFound().finish()
|
|
||||||
}
|
}
|
||||||
} else if let Ok(file) = NamedFile::open(path) {
|
}
|
||||||
|
|
||||||
|
if let Ok(file) = NamedFile::open(&path) {
|
||||||
span.set_status(Status::Ok);
|
span.set_status(Status::Ok);
|
||||||
file.into_response(&request)
|
return file.into_response(&request);
|
||||||
} else {
|
}
|
||||||
|
|
||||||
span.set_status(Status::error("Not found"));
|
span.set_status(Status::error("Not found"));
|
||||||
HttpResponse::NotFound().finish()
|
HttpResponse::NotFound().finish()
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
span.set_status(Status::error("Bad photos request"));
|
span.set_status(Status::error("Bad photos request"));
|
||||||
error!("Bad photos request: {}", req.path);
|
error!("Bad photos request: {}", req.path);
|
||||||
@@ -120,6 +129,17 @@ async fn get_image(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_video_file(path: &Path) -> bool {
|
||||||
|
if let Some(extension) = path.extension() {
|
||||||
|
matches!(
|
||||||
|
extension.to_str().unwrap_or("").to_lowercase().as_str(),
|
||||||
|
"mp4" | "mov" | "avi" | "mkv"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/image/metadata")]
|
#[get("/image/metadata")]
|
||||||
async fn get_file_metadata(
|
async fn get_file_metadata(
|
||||||
_: Claims,
|
_: Claims,
|
||||||
@@ -589,6 +609,7 @@ fn main() -> std::io::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
create_thumbnails();
|
create_thumbnails();
|
||||||
|
generate_video_gifs().await;
|
||||||
|
|
||||||
let app_data = Data::new(AppState::default());
|
let app_data = Data::new(AppState::default());
|
||||||
|
|
||||||
|
|||||||
42
src/state.rs
42
src/state.rs
@@ -1,5 +1,4 @@
|
|||||||
use crate::video::{PlaylistGenerator, VideoPlaylistManager};
|
use crate::video::actors::{PlaylistGenerator, StreamActor, VideoPlaylistManager};
|
||||||
use crate::StreamActor;
|
|
||||||
use actix::{Actor, Addr};
|
use actix::{Actor, Addr};
|
||||||
use std::{env, sync::Arc};
|
use std::{env, sync::Arc};
|
||||||
|
|
||||||
@@ -9,6 +8,7 @@ pub struct AppState {
|
|||||||
pub base_path: String,
|
pub base_path: String,
|
||||||
pub thumbnail_path: String,
|
pub thumbnail_path: String,
|
||||||
pub video_path: String,
|
pub video_path: String,
|
||||||
|
pub gif_path: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
@@ -17,6 +17,7 @@ impl AppState {
|
|||||||
base_path: String,
|
base_path: String,
|
||||||
thumbnail_path: String,
|
thumbnail_path: String,
|
||||||
video_path: String,
|
video_path: String,
|
||||||
|
gif_path: String,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let playlist_generator = PlaylistGenerator::new();
|
let playlist_generator = PlaylistGenerator::new();
|
||||||
let video_playlist_manager =
|
let video_playlist_manager =
|
||||||
@@ -28,8 +29,10 @@ impl AppState {
|
|||||||
base_path,
|
base_path,
|
||||||
thumbnail_path,
|
thumbnail_path,
|
||||||
video_path,
|
video_path,
|
||||||
|
gif_path,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for AppState {
|
impl Default for AppState {
|
||||||
@@ -39,6 +42,41 @@ impl Default for AppState {
|
|||||||
env::var("BASE_PATH").expect("BASE_PATH was not set in the env"),
|
env::var("BASE_PATH").expect("BASE_PATH was not set in the env"),
|
||||||
env::var("THUMBNAILS").expect("THUMBNAILS was not set in the env"),
|
env::var("THUMBNAILS").expect("THUMBNAILS was not set in the env"),
|
||||||
env::var("VIDEO_PATH").expect("VIDEO_PATH was not set in the env"),
|
env::var("VIDEO_PATH").expect("VIDEO_PATH was not set in the env"),
|
||||||
|
env::var("GIFS_DIRECTORY").expect("GIFS_DIRECTORY was not set in the env"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
impl AppState {
|
||||||
|
/// Creates an AppState instance for testing with temporary directories
|
||||||
|
pub fn test_state() -> Self {
|
||||||
|
use actix::Actor;
|
||||||
|
// Create a base temporary directory
|
||||||
|
let temp_dir = tempfile::tempdir().expect("Failed to create temp directory");
|
||||||
|
let base_path = temp_dir.path().to_path_buf();
|
||||||
|
|
||||||
|
// Create subdirectories for thumbnails, videos, and gifs
|
||||||
|
let thumbnail_path = create_test_subdir(&base_path, "thumbnails");
|
||||||
|
let video_path = create_test_subdir(&base_path, "videos");
|
||||||
|
let gif_path = create_test_subdir(&base_path, "gifs");
|
||||||
|
|
||||||
|
// Create the AppState with the temporary paths
|
||||||
|
AppState::new(
|
||||||
|
std::sync::Arc::new(crate::video::actors::StreamActor {}.start()),
|
||||||
|
base_path.to_string_lossy().to_string(),
|
||||||
|
thumbnail_path.to_string_lossy().to_string(),
|
||||||
|
video_path.to_string_lossy().to_string(),
|
||||||
|
gif_path.to_string_lossy().to_string(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper function to create a subdirectory inside the base directory for testing
|
||||||
|
#[cfg(test)]
|
||||||
|
fn create_test_subdir(base_path: &std::path::Path, name: &str) -> std::path::PathBuf {
|
||||||
|
let dir_path = base_path.join(name);
|
||||||
|
std::fs::create_dir_all(&dir_path)
|
||||||
|
.expect(&format!("Failed to create {} directory", name));
|
||||||
|
dir_path
|
||||||
|
}
|
||||||
|
|||||||
54
src/tags.rs
54
src/tags.rs
@@ -299,7 +299,7 @@ impl TagDao for SqliteTagDao {
|
|||||||
) -> anyhow::Result<Vec<(i64, Tag)>> {
|
) -> anyhow::Result<Vec<(i64, Tag)>> {
|
||||||
// select name, count(*) from tags join tagged_photo ON tags.id = tagged_photo.tag_id GROUP BY tags.name ORDER BY COUNT(*);
|
// select name, count(*) from tags join tagged_photo ON tags.id = tagged_photo.tag_id GROUP BY tags.name ORDER BY COUNT(*);
|
||||||
|
|
||||||
trace_db_call(&context, "query", "get_all_tags", |span| {
|
trace_db_call(context, "query", "get_all_tags", |span| {
|
||||||
span.set_attribute(KeyValue::new("path", path.clone().unwrap_or_default()));
|
span.set_attribute(KeyValue::new("path", path.clone().unwrap_or_default()));
|
||||||
|
|
||||||
let path = path.map(|p| p + "%").unwrap_or("%".to_string());
|
let path = path.map(|p| p + "%").unwrap_or("%".to_string());
|
||||||
@@ -334,7 +334,7 @@ impl TagDao for SqliteTagDao {
|
|||||||
context: &opentelemetry::Context,
|
context: &opentelemetry::Context,
|
||||||
path: &str,
|
path: &str,
|
||||||
) -> anyhow::Result<Vec<Tag>> {
|
) -> anyhow::Result<Vec<Tag>> {
|
||||||
trace_db_call(&context, "query", "get_tags_for_path", |span| {
|
trace_db_call(context, "query", "get_tags_for_path", |span| {
|
||||||
span.set_attribute(KeyValue::new("path", path.to_string()));
|
span.set_attribute(KeyValue::new("path", path.to_string()));
|
||||||
|
|
||||||
debug!("Getting Tags for path: {:?}", path);
|
debug!("Getting Tags for path: {:?}", path);
|
||||||
@@ -348,7 +348,7 @@ impl TagDao for SqliteTagDao {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn create_tag(&mut self, context: &opentelemetry::Context, name: &str) -> anyhow::Result<Tag> {
|
fn create_tag(&mut self, context: &opentelemetry::Context, name: &str) -> anyhow::Result<Tag> {
|
||||||
trace_db_call(&context, "insert", "create_tag", |span| {
|
trace_db_call(context, "insert", "create_tag", |span| {
|
||||||
span.set_attribute(KeyValue::new("name", name.to_string()));
|
span.set_attribute(KeyValue::new("name", name.to_string()));
|
||||||
|
|
||||||
diesel::insert_into(tags::table)
|
diesel::insert_into(tags::table)
|
||||||
@@ -386,7 +386,7 @@ impl TagDao for SqliteTagDao {
|
|||||||
tag_name: &str,
|
tag_name: &str,
|
||||||
path: &str,
|
path: &str,
|
||||||
) -> anyhow::Result<Option<()>> {
|
) -> anyhow::Result<Option<()>> {
|
||||||
trace_db_call(&context, "delete", "remove_tag", |span| {
|
trace_db_call(context, "delete", "remove_tag", |span| {
|
||||||
span.set_attributes(vec![
|
span.set_attributes(vec![
|
||||||
KeyValue::new("tag_name", tag_name.to_string()),
|
KeyValue::new("tag_name", tag_name.to_string()),
|
||||||
KeyValue::new("path", path.to_string()),
|
KeyValue::new("path", path.to_string()),
|
||||||
@@ -421,7 +421,7 @@ impl TagDao for SqliteTagDao {
|
|||||||
path: &str,
|
path: &str,
|
||||||
tag_id: i32,
|
tag_id: i32,
|
||||||
) -> anyhow::Result<TaggedPhoto> {
|
) -> anyhow::Result<TaggedPhoto> {
|
||||||
trace_db_call(&context, "insert", "tag_file", |span| {
|
trace_db_call(context, "insert", "tag_file", |span| {
|
||||||
span.set_attributes(vec![
|
span.set_attributes(vec![
|
||||||
KeyValue::new("path", path.to_string()),
|
KeyValue::new("path", path.to_string()),
|
||||||
KeyValue::new("tag_id", tag_id.to_string()),
|
KeyValue::new("tag_id", tag_id.to_string()),
|
||||||
@@ -464,7 +464,7 @@ impl TagDao for SqliteTagDao {
|
|||||||
exclude_tag_ids: Vec<i32>,
|
exclude_tag_ids: Vec<i32>,
|
||||||
context: &opentelemetry::Context,
|
context: &opentelemetry::Context,
|
||||||
) -> anyhow::Result<Vec<FileWithTagCount>> {
|
) -> anyhow::Result<Vec<FileWithTagCount>> {
|
||||||
trace_db_call(&context, "query", "get_files_with_all_tags", |_| {
|
trace_db_call(context, "query", "get_files_with_all_tags", |_| {
|
||||||
use diesel::dsl::*;
|
use diesel::dsl::*;
|
||||||
|
|
||||||
let exclude_subquery = tagged_photo::table
|
let exclude_subquery = tagged_photo::table
|
||||||
@@ -501,7 +501,7 @@ impl TagDao for SqliteTagDao {
|
|||||||
exclude_tag_ids: Vec<i32>,
|
exclude_tag_ids: Vec<i32>,
|
||||||
context: &opentelemetry::Context,
|
context: &opentelemetry::Context,
|
||||||
) -> anyhow::Result<Vec<FileWithTagCount>> {
|
) -> anyhow::Result<Vec<FileWithTagCount>> {
|
||||||
trace_db_call(&context, "query", "get_files_with_any_tags", |_| {
|
trace_db_call(context, "query", "get_files_with_any_tags", |_| {
|
||||||
use diesel::dsl::*;
|
use diesel::dsl::*;
|
||||||
// Create the placeholders for the IN clauses
|
// Create the placeholders for the IN clauses
|
||||||
let tag_placeholders = std::iter::repeat("?")
|
let tag_placeholders = std::iter::repeat("?")
|
||||||
@@ -553,6 +553,7 @@ impl TagDao for SqliteTagDao {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use actix_web::web::{Data, Json};
|
use actix_web::web::{Data, Json};
|
||||||
|
use actix_web::test::TestRequest;
|
||||||
use std::{cell::RefCell, collections::HashMap};
|
use std::{cell::RefCell, collections::HashMap};
|
||||||
|
|
||||||
use diesel::result::Error::NotFound;
|
use diesel::result::Error::NotFound;
|
||||||
@@ -579,7 +580,7 @@ mod tests {
|
|||||||
impl TagDao for TestTagDao {
|
impl TagDao for TestTagDao {
|
||||||
fn get_all_tags(
|
fn get_all_tags(
|
||||||
&mut self,
|
&mut self,
|
||||||
context: &opentelemetry::Context,
|
_context: &opentelemetry::Context,
|
||||||
_option: Option<String>,
|
_option: Option<String>,
|
||||||
) -> anyhow::Result<Vec<(i64, Tag)>> {
|
) -> anyhow::Result<Vec<(i64, Tag)>> {
|
||||||
Ok(self
|
Ok(self
|
||||||
@@ -593,7 +594,7 @@ mod tests {
|
|||||||
|
|
||||||
fn get_tags_for_path(
|
fn get_tags_for_path(
|
||||||
&mut self,
|
&mut self,
|
||||||
context: &opentelemetry::Context,
|
_context: &opentelemetry::Context,
|
||||||
path: &str,
|
path: &str,
|
||||||
) -> anyhow::Result<Vec<Tag>> {
|
) -> anyhow::Result<Vec<Tag>> {
|
||||||
info!("Getting test tags for: {:?}", path);
|
info!("Getting test tags for: {:?}", path);
|
||||||
@@ -609,7 +610,7 @@ mod tests {
|
|||||||
|
|
||||||
fn create_tag(
|
fn create_tag(
|
||||||
&mut self,
|
&mut self,
|
||||||
context: &opentelemetry::Context,
|
_context: &opentelemetry::Context,
|
||||||
name: &str,
|
name: &str,
|
||||||
) -> anyhow::Result<Tag> {
|
) -> anyhow::Result<Tag> {
|
||||||
self.tag_count += 1;
|
self.tag_count += 1;
|
||||||
@@ -629,7 +630,7 @@ mod tests {
|
|||||||
|
|
||||||
fn remove_tag(
|
fn remove_tag(
|
||||||
&mut self,
|
&mut self,
|
||||||
context: &opentelemetry::Context,
|
_context: &opentelemetry::Context,
|
||||||
tag_name: &str,
|
tag_name: &str,
|
||||||
path: &str,
|
path: &str,
|
||||||
) -> anyhow::Result<Option<()>> {
|
) -> anyhow::Result<Option<()>> {
|
||||||
@@ -654,7 +655,7 @@ mod tests {
|
|||||||
|
|
||||||
fn tag_file(
|
fn tag_file(
|
||||||
&mut self,
|
&mut self,
|
||||||
context: &opentelemetry::Context,
|
_context: &opentelemetry::Context,
|
||||||
path: &str,
|
path: &str,
|
||||||
tag_id: i32,
|
tag_id: i32,
|
||||||
) -> anyhow::Result<TaggedPhoto> {
|
) -> anyhow::Result<TaggedPhoto> {
|
||||||
@@ -694,7 +695,7 @@ mod tests {
|
|||||||
&mut self,
|
&mut self,
|
||||||
tag_ids: Vec<i32>,
|
tag_ids: Vec<i32>,
|
||||||
exclude_tag_ids: Vec<i32>,
|
exclude_tag_ids: Vec<i32>,
|
||||||
context: &opentelemetry::Context,
|
_context: &opentelemetry::Context,
|
||||||
) -> anyhow::Result<Vec<FileWithTagCount>> {
|
) -> anyhow::Result<Vec<FileWithTagCount>> {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
@@ -703,7 +704,7 @@ mod tests {
|
|||||||
&mut self,
|
&mut self,
|
||||||
tag_ids: Vec<i32>,
|
tag_ids: Vec<i32>,
|
||||||
exclude_tag_ids: Vec<i32>,
|
exclude_tag_ids: Vec<i32>,
|
||||||
context: &opentelemetry::Context,
|
_context: &opentelemetry::Context,
|
||||||
) -> anyhow::Result<Vec<FileWithTagCount>> {
|
) -> anyhow::Result<Vec<FileWithTagCount>> {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
@@ -719,10 +720,11 @@ mod tests {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let tag_data = Data::new(Mutex::new(tag_dao));
|
let tag_data = Data::new(Mutex::new(tag_dao));
|
||||||
add_tag(claims, web::Json(body), tag_data.clone()).await;
|
let request = TestRequest::default().to_http_request();
|
||||||
|
add_tag(claims, request, web::Json(body), tag_data.clone()).await;
|
||||||
|
|
||||||
let mut tag_dao = tag_data.lock().unwrap();
|
let mut tag_dao = tag_data.lock().unwrap();
|
||||||
let tags = tag_dao.get_all_tags(None).unwrap();
|
let tags = tag_dao.get_all_tags(&opentelemetry::Context::current(), None).unwrap();
|
||||||
assert_eq!(tags.len(), 1);
|
assert_eq!(tags.len(), 1);
|
||||||
assert_eq!(tags.first().unwrap().1.name, "test-tag");
|
assert_eq!(tags.first().unwrap().1.name, "test-tag");
|
||||||
let tagged_photos = tag_dao.tagged_photos.borrow();
|
let tagged_photos = tag_dao.tagged_photos.borrow();
|
||||||
@@ -744,11 +746,12 @@ mod tests {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let tag_data = Data::new(Mutex::new(tag_dao));
|
let tag_data = Data::new(Mutex::new(tag_dao));
|
||||||
add_tag(claims.clone(), web::Json(add_request), tag_data.clone()).await;
|
let request = TestRequest::default().to_http_request();
|
||||||
remove_tagged_photo(claims, web::Json(remove_request), tag_data.clone()).await;
|
add_tag(claims.clone(), request.clone(), web::Json(add_request), tag_data.clone()).await;
|
||||||
|
remove_tagged_photo(claims, request, web::Json(remove_request), tag_data.clone()).await;
|
||||||
|
|
||||||
let mut tag_dao = tag_data.lock().unwrap();
|
let mut tag_dao = tag_data.lock().unwrap();
|
||||||
let tags = tag_dao.get_all_tags(None).unwrap();
|
let tags = tag_dao.get_all_tags(&opentelemetry::Context::current(), None).unwrap();
|
||||||
assert!(tags.is_empty());
|
assert!(tags.is_empty());
|
||||||
let tagged_photos = tag_dao.tagged_photos.borrow();
|
let tagged_photos = tag_dao.tagged_photos.borrow();
|
||||||
let previously_added_tagged_photo = tagged_photos.get("test.png").unwrap();
|
let previously_added_tagged_photo = tagged_photos.get("test.png").unwrap();
|
||||||
@@ -758,12 +761,12 @@ mod tests {
|
|||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn replace_tags_keeps_existing_tags_removes_extras_adds_missing_test() {
|
async fn replace_tags_keeps_existing_tags_removes_extras_adds_missing_test() {
|
||||||
let mut tag_dao = TestTagDao::new();
|
let mut tag_dao = TestTagDao::new();
|
||||||
let new_tag = tag_dao.create_tag("Test").unwrap();
|
let new_tag = tag_dao.create_tag(&opentelemetry::Context::current(), "Test").unwrap();
|
||||||
let new_tag2 = tag_dao.create_tag("Test2").unwrap();
|
let new_tag2 = tag_dao.create_tag(&opentelemetry::Context::current(), "Test2").unwrap();
|
||||||
let _ = tag_dao.create_tag("Test3").unwrap();
|
let _ = tag_dao.create_tag(&opentelemetry::Context::current(), "Test3").unwrap();
|
||||||
|
|
||||||
tag_dao.tag_file("test.jpg", new_tag.id).unwrap();
|
tag_dao.tag_file(&opentelemetry::Context::current(), "test.jpg", new_tag.id).unwrap();
|
||||||
tag_dao.tag_file("test.jpg", new_tag2.id).unwrap();
|
tag_dao.tag_file(&opentelemetry::Context::current(), "test.jpg", new_tag2.id).unwrap();
|
||||||
|
|
||||||
let claims = Claims::valid_user(String::from("1"));
|
let claims = Claims::valid_user(String::from("1"));
|
||||||
let tag_data = Data::new(Mutex::new(tag_dao));
|
let tag_data = Data::new(Mutex::new(tag_dao));
|
||||||
@@ -773,7 +776,8 @@ mod tests {
|
|||||||
file_name: String::from("test.jpg"),
|
file_name: String::from("test.jpg"),
|
||||||
};
|
};
|
||||||
|
|
||||||
update_tags(claims, tag_data.clone(), Json(add_tags_request)).await;
|
let request = TestRequest::default().to_http_request();
|
||||||
|
update_tags(claims, tag_data.clone(), request, web::Json(add_tags_request)).await;
|
||||||
|
|
||||||
let tag_dao = tag_data.lock().unwrap();
|
let tag_dao = tag_data.lock().unwrap();
|
||||||
let tags_for_test_photo = &tag_dao.tagged_photos.borrow()["test.jpg"];
|
let tags_for_test_photo = &tag_dao.tagged_photos.borrow()["test.jpg"];
|
||||||
|
|||||||
185
src/video/ffmpeg.rs
Normal file
185
src/video/ffmpeg.rs
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
use futures::TryFutureExt;
|
||||||
|
use log::{debug, error, info, warn};
|
||||||
|
use std::io::{Result};
|
||||||
|
use std::process::{Output, Stdio};
|
||||||
|
use std::time::Instant;
|
||||||
|
use tokio::process::Command;
|
||||||
|
|
||||||
|
pub struct Ffmpeg;
|
||||||
|
|
||||||
|
pub enum GifType {
|
||||||
|
Overview,
|
||||||
|
OverviewVideo { duration: u32 },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ffmpeg {
|
||||||
|
async fn _generate_playlist(&self, input_file: &str, output_file: &str) -> Result<String> {
|
||||||
|
let ffmpeg_result: Result<Output> = Command::new("ffmpeg")
|
||||||
|
.arg("-i")
|
||||||
|
.arg(input_file)
|
||||||
|
.arg("-c:v")
|
||||||
|
.arg("h264")
|
||||||
|
.arg("-crf")
|
||||||
|
.arg("21")
|
||||||
|
.arg("-preset")
|
||||||
|
.arg("veryfast")
|
||||||
|
.arg("-hls_time")
|
||||||
|
.arg("3")
|
||||||
|
.arg("-hls_list_size")
|
||||||
|
.arg("100")
|
||||||
|
.arg("-vf")
|
||||||
|
.arg("scale=1080:-2,setsar=1:1")
|
||||||
|
.arg(output_file)
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.output()
|
||||||
|
.inspect_err(|e| error!("Failed to run ffmpeg on child process: {}", e))
|
||||||
|
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Ok(ref res) = ffmpeg_result {
|
||||||
|
debug!("ffmpeg output: {:?}", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
ffmpeg_result.map(|_| output_file.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_video_duration(&self, input_file: &str) -> Result<u32> {
|
||||||
|
Command::new("ffprobe")
|
||||||
|
.args(["-i", input_file])
|
||||||
|
.args(["-show_entries", "format=duration"])
|
||||||
|
.args(["-v", "quiet"])
|
||||||
|
.args(["-of", "csv=p=0"])
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
.map(|out| String::from_utf8_lossy(&out.stdout).trim().to_string())
|
||||||
|
.inspect(|duration| debug!("Found video duration: {:?}", duration))
|
||||||
|
.and_then(|duration| {
|
||||||
|
duration
|
||||||
|
.parse::<f32>()
|
||||||
|
.map(|duration| duration as u32)
|
||||||
|
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))
|
||||||
|
})
|
||||||
|
.inspect(|duration| debug!("Found video duration: {:?}", duration))
|
||||||
|
}
|
||||||
|
pub async fn generate_video_gif(
|
||||||
|
&self,
|
||||||
|
input_file: &str,
|
||||||
|
output_file: &str,
|
||||||
|
gif_type: GifType,
|
||||||
|
) -> Result<String> {
|
||||||
|
info!("Creating gif for: '{}'", input_file);
|
||||||
|
|
||||||
|
match gif_type {
|
||||||
|
GifType::Overview => {
|
||||||
|
let temp_dir = tempfile::tempdir()?;
|
||||||
|
let temp_path = temp_dir
|
||||||
|
.path()
|
||||||
|
.to_str()
|
||||||
|
.expect("Unable to make temp_dir a string");
|
||||||
|
|
||||||
|
match self
|
||||||
|
.get_video_duration(input_file)
|
||||||
|
.and_then(|duration| {
|
||||||
|
debug!("Creating gif frames for '{}'", input_file);
|
||||||
|
|
||||||
|
Command::new("ffmpeg")
|
||||||
|
.args(["-i", input_file])
|
||||||
|
.args(["-vf", &format!("fps=20/{}", duration)])
|
||||||
|
.args(["-q:v", "2"])
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.arg(format!("{}/frame_%03d.jpg", temp_path))
|
||||||
|
.status()
|
||||||
|
})
|
||||||
|
.and_then(|_| {
|
||||||
|
debug!("Generating palette");
|
||||||
|
|
||||||
|
Command::new("ffmpeg")
|
||||||
|
.args(["-i", &format!("{}/frame_%03d.jpg", temp_path)])
|
||||||
|
.args(["-vf", "palettegen"])
|
||||||
|
.arg(format!("{}/palette.png", temp_path))
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.status()
|
||||||
|
})
|
||||||
|
.and_then(|_| {
|
||||||
|
debug!("Creating gif for: '{}'", input_file);
|
||||||
|
self.create_gif_from_frames(temp_path, output_file)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(exit_code) => {
|
||||||
|
if exit_code == 0 {
|
||||||
|
info!("Created gif for '{}' -> '{}'", input_file, output_file);
|
||||||
|
} else {
|
||||||
|
warn!(
|
||||||
|
"Failed to create gif for '{}' with exit code: {}",
|
||||||
|
input_file, exit_code
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Error creating gif for '{}': {:?}", input_file, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GifType::OverviewVideo { duration } => {
|
||||||
|
let start = Instant::now();
|
||||||
|
|
||||||
|
match self
|
||||||
|
.get_video_duration(input_file)
|
||||||
|
.and_then(|input_duration| {
|
||||||
|
Command::new("ffmpeg")
|
||||||
|
.args(["-i", input_file])
|
||||||
|
.args([
|
||||||
|
"-vf",
|
||||||
|
// Grab 1 second of frames equally spaced to create a 'duration' second long video scaled to 720px on longest side
|
||||||
|
&format!(
|
||||||
|
"select='lt(mod(t,{}),1)',setpts=N/FRAME_RATE/TB,scale='if(gt(iw,ih),720,-2)':'if(gt(ih,iw),720,-2)",
|
||||||
|
input_duration / duration
|
||||||
|
),
|
||||||
|
])
|
||||||
|
.arg("-an")
|
||||||
|
.arg(output_file)
|
||||||
|
.status()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(out) => info!("Finished clip '{}' with code {:?} in {:?}", output_file, out.code(), start.elapsed()),
|
||||||
|
Err(e) => error!("Error creating video overview: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(output_file.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_gif_from_frames(&self, frame_base_dir: &str, output_file: &str) -> Result<i32> {
|
||||||
|
let output = Command::new("ffmpeg")
|
||||||
|
.arg("-y")
|
||||||
|
.args(["-framerate", "4"])
|
||||||
|
.args(["-i", &format!("{}/frame_%03d.jpg", frame_base_dir)])
|
||||||
|
.args(["-i", &format!("{}/palette.png", frame_base_dir)])
|
||||||
|
.args([
|
||||||
|
"-filter_complex",
|
||||||
|
// Scale to 480x480 with a center crop
|
||||||
|
"[0:v]scale=480:-1:flags=lanczos,crop='min(in_w,in_h)':'min(in_w,in_h)':(in_w-out_w)/2:(in_h-out_h)/2, paletteuse",
|
||||||
|
])
|
||||||
|
.args(["-loop", "0"]) // loop forever
|
||||||
|
.args(["-final_delay", "75"])
|
||||||
|
.arg(output_file)
|
||||||
|
.stderr(Stdio::piped()) // Change this to capture stderr
|
||||||
|
.stdout(Stdio::piped()) // Optionally capture stdout too
|
||||||
|
.output()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
error!("FFmpeg error: {}", stderr);
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
debug!("FFmpeg stdout: {}", stdout);
|
||||||
|
} else {
|
||||||
|
debug!("FFmpeg successful with exit code: {}", output.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(output.status.code().unwrap_or(-1))
|
||||||
|
}
|
||||||
|
}
|
||||||
65
src/video/mod.rs
Normal file
65
src/video/mod.rs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
use crate::otel::global_tracer;
|
||||||
|
use crate::video::ffmpeg::{Ffmpeg, GifType};
|
||||||
|
use crate::{is_video, update_media_counts};
|
||||||
|
use log::{info};
|
||||||
|
use opentelemetry::trace::{Tracer};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::{fs};
|
||||||
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
|
pub mod actors;
|
||||||
|
pub mod ffmpeg;
|
||||||
|
|
||||||
|
pub async fn generate_video_gifs() {
|
||||||
|
tokio::spawn(async {
|
||||||
|
info!("Starting to make video gifs");
|
||||||
|
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
let tracer = global_tracer();
|
||||||
|
tracer.start("creating video gifs");
|
||||||
|
|
||||||
|
let gif_base_path = &dotenv::var("GIFS_DIRECTORY").unwrap_or(String::from("gifs"));
|
||||||
|
let gif_directory: &Path = Path::new(gif_base_path);
|
||||||
|
fs::create_dir_all(gif_base_path).expect("There was an issue creating directory");
|
||||||
|
|
||||||
|
let files = PathBuf::from(dotenv::var("BASE_PATH").unwrap());
|
||||||
|
|
||||||
|
let ffmpeg = Ffmpeg;
|
||||||
|
for file in WalkDir::new(&files)
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|entry| entry.ok())
|
||||||
|
.filter(|entry| entry.file_type().is_file())
|
||||||
|
.filter(is_video)
|
||||||
|
.filter(|entry| {
|
||||||
|
let path = entry.path();
|
||||||
|
let relative_path = &path.strip_prefix(&files).unwrap();
|
||||||
|
let thumb_path = Path::new(gif_directory).join(relative_path);
|
||||||
|
let gif_path = thumb_path.with_extension("gif");
|
||||||
|
!gif_path.exists()
|
||||||
|
})
|
||||||
|
{
|
||||||
|
let path = file.path();
|
||||||
|
let relative_path = &path.strip_prefix(&files).unwrap();
|
||||||
|
let gif_path = Path::new(gif_directory).join(relative_path);
|
||||||
|
let gif_path = gif_path.with_extension("gif");
|
||||||
|
if let Some(parent_dir) = gif_path.parent() {
|
||||||
|
fs::create_dir_all(parent_dir).unwrap_or_else(|_| panic!("There was an issue creating gif directory {:?}",
|
||||||
|
gif_path));
|
||||||
|
}
|
||||||
|
info!("Generating gif for {:?}", path);
|
||||||
|
|
||||||
|
ffmpeg
|
||||||
|
.generate_video_gif(
|
||||||
|
path.to_str().unwrap(),
|
||||||
|
gif_path.to_str().unwrap(),
|
||||||
|
GifType::Overview,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("There was an issue generating the gif");
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Finished making video gifs in {:?}", start.elapsed());
|
||||||
|
|
||||||
|
update_media_counts(&files);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user