Video support
This commit is contained in:
parent
4960527af3
commit
c650c27825
14 changed files with 310 additions and 57 deletions
|
|
@ -2,9 +2,12 @@
|
||||||
|
|
||||||
Sam's small image board. Currently a WIP.
|
Sam's small image board. Currently a WIP.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- `ffmpeg` and `ffprobe`
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
- [ ] Video support
|
|
||||||
- [ ] Cleanup/fixup background tasks
|
- [ ] Cleanup/fixup background tasks
|
||||||
- [ ] User management
|
- [ ] User management
|
||||||
- [ ] CSS
|
- [ ] CSS
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,14 @@ impl MigrationTrait for Migration {
|
||||||
.col(string_len(SameyPost::Media, 255))
|
.col(string_len(SameyPost::Media, 255))
|
||||||
.col(integer(SameyPost::Width))
|
.col(integer(SameyPost::Width))
|
||||||
.col(integer(SameyPost::Height))
|
.col(integer(SameyPost::Height))
|
||||||
|
.col(enumeration(
|
||||||
|
SameyPost::MediaType,
|
||||||
|
MediaType::Enum,
|
||||||
|
[MediaType::Image, MediaType::Video],
|
||||||
|
))
|
||||||
.col(string_len(SameyPost::Thumbnail, 255))
|
.col(string_len(SameyPost::Thumbnail, 255))
|
||||||
|
.col(integer(SameyPost::ThumbnailWidth))
|
||||||
|
.col(integer(SameyPost::ThumbnailHeight))
|
||||||
.col(string_len_null(SameyPost::Title, 100))
|
.col(string_len_null(SameyPost::Title, 100))
|
||||||
.col(text_null(SameyPost::Description))
|
.col(text_null(SameyPost::Description))
|
||||||
.col(boolean(SameyPost::IsPublic).default(false))
|
.col(boolean(SameyPost::IsPublic).default(false))
|
||||||
|
|
@ -291,7 +298,10 @@ enum SameyPost {
|
||||||
Media,
|
Media,
|
||||||
Width,
|
Width,
|
||||||
Height,
|
Height,
|
||||||
|
MediaType,
|
||||||
Thumbnail,
|
Thumbnail,
|
||||||
|
ThumbnailWidth,
|
||||||
|
ThumbnailHeight,
|
||||||
Title,
|
Title,
|
||||||
Description,
|
Description,
|
||||||
IsPublic,
|
IsPublic,
|
||||||
|
|
@ -300,6 +310,17 @@ enum SameyPost {
|
||||||
ParentId,
|
ParentId,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(DeriveIden)]
|
||||||
|
#[sea_orm(enum_name = "media_type")]
|
||||||
|
pub enum MediaType {
|
||||||
|
#[sea_orm(iden = "media_type")]
|
||||||
|
Enum,
|
||||||
|
#[sea_orm(iden = "image")]
|
||||||
|
Image,
|
||||||
|
#[sea_orm(iden = "video")]
|
||||||
|
Video,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(DeriveIden)]
|
#[derive(DeriveIden)]
|
||||||
#[sea_orm(enum_name = "rating")]
|
#[sea_orm(enum_name = "rating")]
|
||||||
pub enum Rating {
|
pub enum Rating {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,11 @@ pub struct Model {
|
||||||
pub media: String,
|
pub media: String,
|
||||||
pub width: i32,
|
pub width: i32,
|
||||||
pub height: i32,
|
pub height: i32,
|
||||||
|
#[sea_orm(column_type = "custom(\"enum_text\")")]
|
||||||
|
pub media_type: String,
|
||||||
pub thumbnail: String,
|
pub thumbnail: String,
|
||||||
|
pub thumbnail_width: i32,
|
||||||
|
pub thumbnail_height: i32,
|
||||||
pub title: Option<String>,
|
pub title: Option<String>,
|
||||||
#[sea_orm(column_type = "Text", nullable)]
|
#[sea_orm(column_type = "Text", nullable)]
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ pub(crate) mod entities;
|
||||||
pub(crate) mod error;
|
pub(crate) mod error;
|
||||||
pub(crate) mod query;
|
pub(crate) mod query;
|
||||||
pub(crate) mod rating;
|
pub(crate) mod rating;
|
||||||
|
pub(crate) mod video;
|
||||||
pub(crate) mod views;
|
pub(crate) mod views;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ pub(crate) struct PostOverview {
|
||||||
pub(crate) id: i32,
|
pub(crate) id: i32,
|
||||||
pub(crate) thumbnail: String,
|
pub(crate) thumbnail: String,
|
||||||
pub(crate) tags: String,
|
pub(crate) tags: String,
|
||||||
|
pub(crate) media_type: String,
|
||||||
pub(crate) rating: String,
|
pub(crate) rating: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,6 +55,7 @@ pub(crate) fn search_posts(
|
||||||
.column(samey_post::Column::Id)
|
.column(samey_post::Column::Id)
|
||||||
.column(samey_post::Column::Thumbnail)
|
.column(samey_post::Column::Thumbnail)
|
||||||
.column(samey_post::Column::Rating)
|
.column(samey_post::Column::Rating)
|
||||||
|
.column(samey_post::Column::MediaType)
|
||||||
.column_as(
|
.column_as(
|
||||||
Expr::cust("GROUP_CONCAT(\"samey_tag\".\"name\", ' ')"),
|
Expr::cust("GROUP_CONCAT(\"samey_tag\".\"name\", ' ')"),
|
||||||
"tags",
|
"tags",
|
||||||
|
|
@ -76,6 +78,7 @@ pub(crate) fn search_posts(
|
||||||
.column(samey_post::Column::Id)
|
.column(samey_post::Column::Id)
|
||||||
.column(samey_post::Column::Thumbnail)
|
.column(samey_post::Column::Thumbnail)
|
||||||
.column(samey_post::Column::Rating)
|
.column(samey_post::Column::Rating)
|
||||||
|
.column(samey_post::Column::MediaType)
|
||||||
.column_as(
|
.column_as(
|
||||||
Expr::cust("GROUP_CONCAT(\"samey_tag\".\"name\", ' ')"),
|
Expr::cust("GROUP_CONCAT(\"samey_tag\".\"name\", ' ')"),
|
||||||
"tags",
|
"tags",
|
||||||
|
|
|
||||||
73
src/video.rs
Normal file
73
src/video.rs
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
|
||||||
|
use crate::SameyError;
|
||||||
|
|
||||||
|
pub(crate) fn generate_thumbnail(
|
||||||
|
input_path: &str,
|
||||||
|
output_path: &str,
|
||||||
|
max_thumbnail_dimension: u32,
|
||||||
|
) -> Result<(), SameyError> {
|
||||||
|
let status = Command::new("ffmpeg")
|
||||||
|
.args(&[
|
||||||
|
"-i",
|
||||||
|
input_path,
|
||||||
|
"-vf",
|
||||||
|
"thumbnail",
|
||||||
|
"-vf",
|
||||||
|
&format!(
|
||||||
|
"scale={}:{}:force_original_aspect_ratio=decrease",
|
||||||
|
max_thumbnail_dimension, max_thumbnail_dimension
|
||||||
|
),
|
||||||
|
"-frames:v",
|
||||||
|
"1",
|
||||||
|
"-q:v",
|
||||||
|
"2", // Quality (2 is good)
|
||||||
|
output_path,
|
||||||
|
])
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.status()?;
|
||||||
|
|
||||||
|
if status.success() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(SameyError::Other(
|
||||||
|
"FFmpeg failed to generate thumbnail".into(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_dimensions_for_video(input_path: &str) -> Result<(u32, u32), SameyError> {
|
||||||
|
let output = Command::new("ffprobe")
|
||||||
|
.args(&[
|
||||||
|
"-v",
|
||||||
|
"error",
|
||||||
|
"-select_streams",
|
||||||
|
"v:0",
|
||||||
|
"-show_entries",
|
||||||
|
"stream=width,height",
|
||||||
|
"-of",
|
||||||
|
"default=nw=1:nk=1",
|
||||||
|
input_path,
|
||||||
|
])
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
return Err(SameyError::Other(
|
||||||
|
"FFprobe failed to get dimensions for video".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let output_str = String::from_utf8_lossy(&output.stdout);
|
||||||
|
|
||||||
|
let mut dimensions = output_str
|
||||||
|
.lines()
|
||||||
|
.filter_map(|line| line.trim().parse().ok());
|
||||||
|
|
||||||
|
match (dimensions.next(), dimensions.next()) {
|
||||||
|
(Some(width), Some(height)) => Ok((width, height)),
|
||||||
|
_ => Err(SameyError::Other("Failed to parse FFprobe output".into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
216
src/views.rs
216
src/views.rs
|
|
@ -4,6 +4,7 @@ use std::{
|
||||||
fs::OpenOptions,
|
fs::OpenOptions,
|
||||||
io::{BufReader, Seek, Write},
|
io::{BufReader, Seek, Write},
|
||||||
num::NonZero,
|
num::NonZero,
|
||||||
|
str::FromStr,
|
||||||
};
|
};
|
||||||
|
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
|
|
@ -22,7 +23,7 @@ use sea_orm::{
|
||||||
ModelTrait, PaginatorTrait, QueryFilter, QuerySelect,
|
ModelTrait, PaginatorTrait, QueryFilter, QuerySelect,
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tokio::task::spawn_blocking;
|
use tokio::{task::spawn_blocking, try_join};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
AppState, NEGATIVE_PREFIX, RATING_PREFIX,
|
AppState, NEGATIVE_PREFIX, RATING_PREFIX,
|
||||||
|
|
@ -40,6 +41,7 @@ use crate::{
|
||||||
query::{
|
query::{
|
||||||
PoolPost, PostOverview, filter_by_user, get_posts_in_pool, get_tags_for_post, search_posts,
|
PoolPost, PostOverview, filter_by_user, get_posts_in_pool, get_tags_for_post, search_posts,
|
||||||
},
|
},
|
||||||
|
video::{generate_thumbnail, get_dimensions_for_video},
|
||||||
};
|
};
|
||||||
|
|
||||||
const MAX_THUMBNAIL_DIMENSION: u32 = 192;
|
const MAX_THUMBNAIL_DIMENSION: u32 = 192;
|
||||||
|
|
@ -98,6 +100,39 @@ pub(crate) async fn logout(mut auth_session: AuthSession) -> Result<impl IntoRes
|
||||||
|
|
||||||
// Post upload view
|
// Post upload view
|
||||||
|
|
||||||
|
enum Format {
|
||||||
|
Video(&'static str),
|
||||||
|
Image(ImageFormat),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Format {
|
||||||
|
fn media_type(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Format::Video(_) => "video",
|
||||||
|
Format::Image(_) => "image",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Format {
|
||||||
|
type Err = SameyError;
|
||||||
|
|
||||||
|
fn from_str(content_type: &str) -> Result<Self, Self::Err> {
|
||||||
|
match content_type {
|
||||||
|
"video/mp4" => Ok(Self::Video(".mp4")),
|
||||||
|
"video/webm" => Ok(Self::Video(".webm")),
|
||||||
|
"application/x-matroska" | "video/mastroska" => Ok(Self::Video(".mkv")),
|
||||||
|
"video/quicktime" => Ok(Self::Video(".mov")),
|
||||||
|
_ => Ok(Self::Image(
|
||||||
|
ImageFormat::from_mime_type(content_type).ok_or(SameyError::Other(format!(
|
||||||
|
"Unknown content type: {}",
|
||||||
|
content_type
|
||||||
|
)))?,
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) async fn upload(
|
pub(crate) async fn upload(
|
||||||
State(AppState { db, files_dir, .. }): State<AppState>,
|
State(AppState { db, files_dir, .. }): State<AppState>,
|
||||||
auth_session: AuthSession,
|
auth_session: AuthSession,
|
||||||
|
|
@ -110,9 +145,12 @@ pub(crate) async fn upload(
|
||||||
|
|
||||||
let mut upload_tags: Option<Vec<samey_tag::Model>> = None;
|
let mut upload_tags: Option<Vec<samey_tag::Model>> = None;
|
||||||
let mut source_file: Option<String> = None;
|
let mut source_file: Option<String> = None;
|
||||||
let mut thumbnail_file: Option<String> = None;
|
let mut media_type: Option<&'static str> = None;
|
||||||
let mut width: Option<NonZero<i32>> = None;
|
let mut width: Option<NonZero<i32>> = None;
|
||||||
let mut height: Option<NonZero<i32>> = None;
|
let mut height: Option<NonZero<i32>> = None;
|
||||||
|
let mut thumbnail_file: Option<String> = None;
|
||||||
|
let mut thumbnail_width: Option<NonZero<i32>> = None;
|
||||||
|
let mut thumbnail_height: Option<NonZero<i32>> = None;
|
||||||
let base_path = std::path::Path::new(files_dir.as_ref());
|
let base_path = std::path::Path::new(files_dir.as_ref());
|
||||||
|
|
||||||
// Read multipart form data
|
// Read multipart form data
|
||||||
|
|
@ -143,73 +181,150 @@ pub(crate) async fn upload(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"media-file" => {
|
"media-file" => {
|
||||||
let content_type = field
|
let content_type = field
|
||||||
.content_type()
|
.content_type()
|
||||||
.ok_or(SameyError::Other("Missing content type".into()))?;
|
.ok_or(SameyError::Other("Missing content type".into()))?;
|
||||||
let format = ImageFormat::from_mime_type(content_type).ok_or(SameyError::Other(
|
match Format::from_str(content_type)? {
|
||||||
format!("Unknown content type: {}", content_type),
|
format @ Format::Video(video_format) => {
|
||||||
))?;
|
media_type = Some(format.media_type());
|
||||||
let file_name = {
|
let thumbnail_format = ImageFormat::Png;
|
||||||
let mut rng = rand::rng();
|
let (file_name, thumbnail_file_name) = {
|
||||||
let mut file_name: String = (0..8)
|
let mut rng = rand::rng();
|
||||||
.map(|_| rng.sample(rand::distr::Alphanumeric) as char)
|
let mut file_name: String = (0..8)
|
||||||
.collect();
|
.map(|_| rng.sample(rand::distr::Alphanumeric) as char)
|
||||||
file_name.push('.');
|
.collect();
|
||||||
file_name.push_str(format.extensions_str()[0]);
|
let thumbnail_file_name = format!(
|
||||||
file_name
|
"thumb-{}.{}",
|
||||||
};
|
file_name,
|
||||||
let thumbnail_file_name = format!("thumb-{}", file_name);
|
thumbnail_format.extensions_str()[0]
|
||||||
let file_path = base_path.join(&file_name);
|
);
|
||||||
let mut file = OpenOptions::new()
|
file_name.push_str(video_format);
|
||||||
.read(true)
|
(file_name, thumbnail_file_name)
|
||||||
.write(true)
|
};
|
||||||
.create(true)
|
let file_path = base_path.join(&file_name);
|
||||||
.open(&file_path)?;
|
let mut file = OpenOptions::new()
|
||||||
while let Some(chunk) = field.chunk().await? {
|
.read(true)
|
||||||
file.write_all(&chunk)?;
|
.write(true)
|
||||||
|
.create(true)
|
||||||
|
.open(&file_path)?;
|
||||||
|
while let Some(chunk) = field.chunk().await? {
|
||||||
|
file.write_all(&chunk)?;
|
||||||
|
}
|
||||||
|
let file_path_2 = file_path.to_string_lossy().into_owned();
|
||||||
|
let thumbnail_path = base_path.join(&thumbnail_file_name);
|
||||||
|
let jh_thumbnail = spawn_blocking(move || {
|
||||||
|
generate_thumbnail(
|
||||||
|
&file_path_2,
|
||||||
|
&thumbnail_path.to_string_lossy(),
|
||||||
|
MAX_THUMBNAIL_DIMENSION,
|
||||||
|
)?;
|
||||||
|
let mut image = ImageReader::new(BufReader::new(
|
||||||
|
OpenOptions::new().read(true).open(thumbnail_path)?,
|
||||||
|
));
|
||||||
|
image.set_format(thumbnail_format);
|
||||||
|
Ok(image.into_dimensions()?)
|
||||||
|
});
|
||||||
|
let file_path_2 = file_path.to_string_lossy().into_owned();
|
||||||
|
let jh_video =
|
||||||
|
spawn_blocking(move || get_dimensions_for_video(&file_path_2));
|
||||||
|
let (dim_thumbnail, dim_video) = match try_join!(jh_thumbnail, jh_video)? {
|
||||||
|
(Ok(dim_thumbnail), Ok(dim_video)) => (dim_thumbnail, dim_video),
|
||||||
|
(Err(err), _) | (_, Err(err)) => return Err(err),
|
||||||
|
};
|
||||||
|
width = NonZero::new(dim_video.0.try_into()?);
|
||||||
|
height = NonZero::new(dim_video.1.try_into()?);
|
||||||
|
thumbnail_width = NonZero::new(dim_thumbnail.0.try_into()?);
|
||||||
|
thumbnail_height = NonZero::new(dim_thumbnail.1.try_into()?);
|
||||||
|
source_file = Some(file_name);
|
||||||
|
thumbnail_file = Some(thumbnail_file_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
format @ Format::Image(image_format) => {
|
||||||
|
media_type = Some(format.media_type());
|
||||||
|
let file_name = {
|
||||||
|
let mut rng = rand::rng();
|
||||||
|
let mut file_name: String = (0..8)
|
||||||
|
.map(|_| rng.sample(rand::distr::Alphanumeric) as char)
|
||||||
|
.collect();
|
||||||
|
file_name.push('.');
|
||||||
|
file_name.push_str(image_format.extensions_str()[0]);
|
||||||
|
file_name
|
||||||
|
};
|
||||||
|
let thumbnail_file_name = format!("thumb-{}", file_name);
|
||||||
|
let file_path = base_path.join(&file_name);
|
||||||
|
let mut file = OpenOptions::new()
|
||||||
|
.read(true)
|
||||||
|
.write(true)
|
||||||
|
.create(true)
|
||||||
|
.open(&file_path)?;
|
||||||
|
while let Some(chunk) = field.chunk().await? {
|
||||||
|
file.write_all(&chunk)?;
|
||||||
|
}
|
||||||
|
let base_path_2 = base_path.to_owned();
|
||||||
|
let thumbnail_path = base_path_2.join(&thumbnail_file_name);
|
||||||
|
let (w, h, tw, th) = spawn_blocking(move || -> Result<_, SameyError> {
|
||||||
|
file.seek(std::io::SeekFrom::Start(0))?;
|
||||||
|
let mut image = ImageReader::new(BufReader::new(file));
|
||||||
|
image.set_format(image_format);
|
||||||
|
let image = image.decode()?;
|
||||||
|
let (w, h) = image.dimensions();
|
||||||
|
let width = NonZero::new(w.try_into()?);
|
||||||
|
let height = NonZero::new(h.try_into()?);
|
||||||
|
let thumbnail = image.resize(
|
||||||
|
MAX_THUMBNAIL_DIMENSION,
|
||||||
|
MAX_THUMBNAIL_DIMENSION,
|
||||||
|
image::imageops::FilterType::CatmullRom,
|
||||||
|
);
|
||||||
|
thumbnail.save(thumbnail_path)?;
|
||||||
|
let (tw, th) = image.dimensions();
|
||||||
|
let thumbnail_width = NonZero::new(tw.try_into()?);
|
||||||
|
let thumbnail_height = NonZero::new(th.try_into()?);
|
||||||
|
Ok((width, height, thumbnail_width, thumbnail_height))
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
width = w;
|
||||||
|
height = h;
|
||||||
|
thumbnail_width = tw;
|
||||||
|
thumbnail_height = th;
|
||||||
|
source_file = Some(file_name);
|
||||||
|
thumbnail_file = Some(thumbnail_file_name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let base_path_2 = base_path.to_owned();
|
|
||||||
let (w, h, thumbnail_file_name) =
|
|
||||||
spawn_blocking(move || -> Result<_, SameyError> {
|
|
||||||
file.seek(std::io::SeekFrom::Start(0))?;
|
|
||||||
let mut image = ImageReader::new(BufReader::new(file));
|
|
||||||
image.set_format(format);
|
|
||||||
let image = image.decode()?;
|
|
||||||
let (w, h) = image.dimensions();
|
|
||||||
let width = NonZero::new(w.try_into()?);
|
|
||||||
let height = NonZero::new(h.try_into()?);
|
|
||||||
let thumbnail = image.resize(
|
|
||||||
MAX_THUMBNAIL_DIMENSION,
|
|
||||||
MAX_THUMBNAIL_DIMENSION,
|
|
||||||
image::imageops::FilterType::CatmullRom,
|
|
||||||
);
|
|
||||||
thumbnail.save(base_path_2.join(&thumbnail_file_name))?;
|
|
||||||
Ok((width, height, thumbnail_file_name))
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
width = w;
|
|
||||||
height = h;
|
|
||||||
source_file = Some(file_name);
|
|
||||||
thumbnail_file = Some(thumbnail_file_name);
|
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let (Some(upload_tags), Some(source_file), Some(thumbnail_file), Some(width), Some(height)) = (
|
if let (
|
||||||
|
Some(upload_tags),
|
||||||
|
Some(source_file),
|
||||||
|
Some(media_type),
|
||||||
|
Some(thumbnail_file),
|
||||||
|
Some(width),
|
||||||
|
Some(height),
|
||||||
|
Some(thumbnail_width),
|
||||||
|
Some(thumbnail_height),
|
||||||
|
) = (
|
||||||
upload_tags,
|
upload_tags,
|
||||||
source_file,
|
source_file,
|
||||||
|
media_type,
|
||||||
thumbnail_file,
|
thumbnail_file,
|
||||||
width.map(|w| w.get()),
|
width.map(|w| w.get()),
|
||||||
height.map(|h| h.get()),
|
height.map(|h| h.get()),
|
||||||
|
thumbnail_width.map(|w| w.get()),
|
||||||
|
thumbnail_height.map(|h| h.get()),
|
||||||
) {
|
) {
|
||||||
let uploaded_post = SameyPost::insert(samey_post::ActiveModel {
|
let uploaded_post = SameyPost::insert(samey_post::ActiveModel {
|
||||||
uploader_id: Set(user.id),
|
uploader_id: Set(user.id),
|
||||||
media: Set(source_file),
|
media: Set(source_file),
|
||||||
|
media_type: Set(media_type.into()),
|
||||||
width: Set(width),
|
width: Set(width),
|
||||||
height: Set(height),
|
height: Set(height),
|
||||||
thumbnail: Set(thumbnail_file),
|
thumbnail: Set(thumbnail_file),
|
||||||
|
thumbnail_width: Set(thumbnail_width),
|
||||||
|
thumbnail_height: Set(thumbnail_height),
|
||||||
title: Set(None),
|
title: Set(None),
|
||||||
description: Set(None),
|
description: Set(None),
|
||||||
rating: Set("u".to_owned()),
|
rating: Set("u".to_owned()),
|
||||||
|
|
@ -934,6 +1049,7 @@ pub(crate) async fn view_post(
|
||||||
.map(|tag| &tag.name)
|
.map(|tag| &tag.name)
|
||||||
.join(" "),
|
.join(" "),
|
||||||
rating: parent_post.rating,
|
rating: parent_post.rating,
|
||||||
|
media_type: parent_post.media_type,
|
||||||
}),
|
}),
|
||||||
None => None,
|
None => None,
|
||||||
}
|
}
|
||||||
|
|
@ -960,6 +1076,7 @@ pub(crate) async fn view_post(
|
||||||
.map(|tag| &tag.name)
|
.map(|tag| &tag.name)
|
||||||
.join(" "),
|
.join(" "),
|
||||||
rating: child_post.rating,
|
rating: child_post.rating,
|
||||||
|
media_type: child_post.media_type,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1094,6 +1211,7 @@ pub(crate) async fn submit_post_details(
|
||||||
.map(|tag| &tag.name)
|
.map(|tag| &tag.name)
|
||||||
.join(" "),
|
.join(" "),
|
||||||
rating: parent_post.rating,
|
rating: parent_post.rating,
|
||||||
|
media_type: parent_post.media_type,
|
||||||
}),
|
}),
|
||||||
None => None,
|
None => None,
|
||||||
}
|
}
|
||||||
|
|
@ -1263,7 +1381,7 @@ pub(crate) async fn remove_field() -> impl IntoResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "get_media.html")]
|
#[template(path = "get_image_media.html")]
|
||||||
struct GetMediaTemplate {
|
struct GetMediaTemplate {
|
||||||
post: samey_post::Model,
|
post: samey_post::Model,
|
||||||
}
|
}
|
||||||
|
|
@ -1291,7 +1409,7 @@ pub(crate) async fn get_media(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "get_full_media.html")]
|
#[template(path = "get_full_image_media.html")]
|
||||||
struct GetFullMediaTemplate {
|
struct GetFullMediaTemplate {
|
||||||
post: samey_post::Model,
|
post: samey_post::Model,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
6
templates/get_video_media.html
Normal file
6
templates/get_video_media.html
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<video
|
||||||
|
id="media"
|
||||||
|
src="/files/{{ post.media }}"
|
||||||
|
controls="controls"
|
||||||
|
style="width: 100%; height: 100%; max-width: {{ post.width }}px; max-height: {{ post.height }}px; aspect-ratio: {{ post.width }} / {{ post.height }}"
|
||||||
|
/>
|
||||||
|
|
@ -53,7 +53,7 @@
|
||||||
type="file"
|
type="file"
|
||||||
id="media-file"
|
id="media-file"
|
||||||
name="media-file"
|
name="media-file"
|
||||||
accept=".jpg, .jpeg, .png, .webp, .gif"
|
accept=".jpg, .jpeg, .png, .webp, .gif, .mp4, .webm, .mkv, .mov"
|
||||||
/>
|
/>
|
||||||
<button type="submit">Submit</button>
|
<button type="submit">Submit</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,10 @@
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Type</label>
|
||||||
|
{{ post.media_type | capitalize }}
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label>Width</label>
|
<label>Width</label>
|
||||||
{{ post.width }}px
|
{{ post.width }}px
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@
|
||||||
>
|
>
|
||||||
<img src="/files/{{ post.thumbnail }}" />
|
<img src="/files/{{ post.thumbnail }}" />
|
||||||
<div>{{ post.rating | upper }}</div>
|
<div>{{ post.rating | upper }}</div>
|
||||||
|
<div>{{ post.media_type }}</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,12 @@
|
||||||
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||||
<title>Post #{{ post.id }} - {{ application_name }}</title>
|
<title>Post #{{ post.id }} - {{ application_name }}</title>
|
||||||
<meta name="generator" content="Samey {{ env!("CARGO_PKG_VERSION") }}" />
|
<meta name="generator" content="Samey {{ env!("CARGO_PKG_VERSION") }}" />
|
||||||
|
<meta property="og:site_name" content="{{ application_name }}" />
|
||||||
<meta
|
<meta
|
||||||
property="og:title"
|
property="og:title"
|
||||||
content="{% if let Some(title) = post.title %}{{ title }}{% endif %}"
|
content="{% if let Some(title) = post.title %}{{ title }}{% endif %}"
|
||||||
/>
|
/>
|
||||||
<meta property="og:url" content="/post/{{ post.id }}" />
|
<meta property="og:url" content="/post/{{ post.id }}" />
|
||||||
<meta property="og:image" content="/files/{{ post.media }}" />
|
|
||||||
<meta property="og:image:width" content="{{ post.width }}" />
|
|
||||||
<meta property="og:image:height" content="{{ post.height }}" />
|
|
||||||
<meta property="og:image:alt" content="{{ tags_text }}" />
|
|
||||||
<meta
|
<meta
|
||||||
property="og:description"
|
property="og:description"
|
||||||
content="{% if let Some(description) = post.description %}{{ description }}{% endif %}"
|
content="{% if let Some(description) = post.description %}{{ description }}{% endif %}"
|
||||||
|
|
@ -23,13 +20,33 @@
|
||||||
property="twitter:title"
|
property="twitter:title"
|
||||||
content="{% if let Some(title) = post.title %}{{ title }}{% endif %}"
|
content="{% if let Some(title) = post.title %}{{ title }}{% endif %}"
|
||||||
/>
|
/>
|
||||||
<meta property="twitter:image:src" content="/files/{{ post.media }}" />
|
{% match post.media_type.as_ref() %} {% when "image" %}
|
||||||
|
<meta property="og:image" content="/files/{{ post.media }}" />
|
||||||
|
<meta property="og:image:width" content="{{ post.width }}" />
|
||||||
|
<meta property="og:image:height" content="{{ post.height }}" />
|
||||||
|
<meta property="og:image:alt" content="{{ tags_text }}" />
|
||||||
<meta property="twitter:card" content="summary_large_image" />
|
<meta property="twitter:card" content="summary_large_image" />
|
||||||
|
{% when "video" %}
|
||||||
|
<meta property="og:video" content="/files/{{ post.media }}" />
|
||||||
|
<meta property="og:video:width" content="{{ post.width }}" />
|
||||||
|
<meta property="og:video:height" content="{{ post.height }}" />
|
||||||
|
<meta property="og:video:alt" content="{{ tags_text }}" />
|
||||||
|
<meta property="og:video:type" content="video/mp4" />
|
||||||
|
<meta property="twitter:card" content="player" />
|
||||||
|
<meta name="twitter:player" content="/files/{{ post.media }}" />
|
||||||
|
<meta name="twitter:player:width" content="{{ post.width }}" />
|
||||||
|
<meta name="twitter:player:height" content="{{ post.height }}" />
|
||||||
|
<meta name="twitter:image" content="/files/{{ post.thumbnail }}" />
|
||||||
|
{% else %} {% endmatch %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<main>
|
||||||
<h1>View post #{{ post.id }}</h1>
|
<h1>View post #{{ post.id }}</h1>
|
||||||
<div>{% include "get_media.html" %}</div>
|
<div>
|
||||||
|
{% match post.media_type.as_ref() %}{% when "image" %}{% include
|
||||||
|
"get_image_media.html" %}{% when "video" %}{% include
|
||||||
|
"get_video_media.html" %}{% else %}{% endmatch %}
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<article>
|
<article>
|
||||||
<h2>Details</h2>
|
<h2>Details</h2>
|
||||||
|
|
@ -41,6 +58,7 @@
|
||||||
<a href="/post/{{ parent_post.id }}" title="{{ parent_post.tags }}">
|
<a href="/post/{{ parent_post.id }}" title="{{ parent_post.tags }}">
|
||||||
<img src="/files/{{ parent_post.thumbnail }}" />
|
<img src="/files/{{ parent_post.thumbnail }}" />
|
||||||
<div>{{ parent_post.rating }}</div>
|
<div>{{ parent_post.rating }}</div>
|
||||||
|
<div>{{ parent_post.media_type }}</div>
|
||||||
</a>
|
</a>
|
||||||
</article>
|
</article>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
@ -54,6 +72,7 @@
|
||||||
<a href="/post/{{ child_post.id }}" title="{{ child_post.tags }}">
|
<a href="/post/{{ child_post.id }}" title="{{ child_post.tags }}">
|
||||||
<img src="/files/{{ child_post.thumbnail }}" />
|
<img src="/files/{{ child_post.thumbnail }}" />
|
||||||
<div>{{ child_post.rating | upper }}</div>
|
<div>{{ child_post.rating | upper }}</div>
|
||||||
|
<div>{{ child_post.media_type }}</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue