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.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `ffmpeg` and `ffprobe`
|
||||
|
||||
## TODO
|
||||
|
||||
- [ ] Video support
|
||||
- [ ] Cleanup/fixup background tasks
|
||||
- [ ] User management
|
||||
- [ ] CSS
|
||||
|
|
|
|||
|
|
@ -86,7 +86,14 @@ impl MigrationTrait for Migration {
|
|||
.col(string_len(SameyPost::Media, 255))
|
||||
.col(integer(SameyPost::Width))
|
||||
.col(integer(SameyPost::Height))
|
||||
.col(enumeration(
|
||||
SameyPost::MediaType,
|
||||
MediaType::Enum,
|
||||
[MediaType::Image, MediaType::Video],
|
||||
))
|
||||
.col(string_len(SameyPost::Thumbnail, 255))
|
||||
.col(integer(SameyPost::ThumbnailWidth))
|
||||
.col(integer(SameyPost::ThumbnailHeight))
|
||||
.col(string_len_null(SameyPost::Title, 100))
|
||||
.col(text_null(SameyPost::Description))
|
||||
.col(boolean(SameyPost::IsPublic).default(false))
|
||||
|
|
@ -291,7 +298,10 @@ enum SameyPost {
|
|||
Media,
|
||||
Width,
|
||||
Height,
|
||||
MediaType,
|
||||
Thumbnail,
|
||||
ThumbnailWidth,
|
||||
ThumbnailHeight,
|
||||
Title,
|
||||
Description,
|
||||
IsPublic,
|
||||
|
|
@ -300,6 +310,17 @@ enum SameyPost {
|
|||
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)]
|
||||
#[sea_orm(enum_name = "rating")]
|
||||
pub enum Rating {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,11 @@ pub struct Model {
|
|||
pub media: String,
|
||||
pub width: i32,
|
||||
pub height: i32,
|
||||
#[sea_orm(column_type = "custom(\"enum_text\")")]
|
||||
pub media_type: String,
|
||||
pub thumbnail: String,
|
||||
pub thumbnail_width: i32,
|
||||
pub thumbnail_height: i32,
|
||||
pub title: Option<String>,
|
||||
#[sea_orm(column_type = "Text", nullable)]
|
||||
pub description: Option<String>,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ pub(crate) mod entities;
|
|||
pub(crate) mod error;
|
||||
pub(crate) mod query;
|
||||
pub(crate) mod rating;
|
||||
pub(crate) mod video;
|
||||
pub(crate) mod views;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ pub(crate) struct PostOverview {
|
|||
pub(crate) id: i32,
|
||||
pub(crate) thumbnail: String,
|
||||
pub(crate) tags: String,
|
||||
pub(crate) media_type: String,
|
||||
pub(crate) rating: String,
|
||||
}
|
||||
|
||||
|
|
@ -54,6 +55,7 @@ pub(crate) fn search_posts(
|
|||
.column(samey_post::Column::Id)
|
||||
.column(samey_post::Column::Thumbnail)
|
||||
.column(samey_post::Column::Rating)
|
||||
.column(samey_post::Column::MediaType)
|
||||
.column_as(
|
||||
Expr::cust("GROUP_CONCAT(\"samey_tag\".\"name\", ' ')"),
|
||||
"tags",
|
||||
|
|
@ -76,6 +78,7 @@ pub(crate) fn search_posts(
|
|||
.column(samey_post::Column::Id)
|
||||
.column(samey_post::Column::Thumbnail)
|
||||
.column(samey_post::Column::Rating)
|
||||
.column(samey_post::Column::MediaType)
|
||||
.column_as(
|
||||
Expr::cust("GROUP_CONCAT(\"samey_tag\".\"name\", ' ')"),
|
||||
"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,
|
||||
io::{BufReader, Seek, Write},
|
||||
num::NonZero,
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use askama::Template;
|
||||
|
|
@ -22,7 +23,7 @@ use sea_orm::{
|
|||
ModelTrait, PaginatorTrait, QueryFilter, QuerySelect,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use tokio::task::spawn_blocking;
|
||||
use tokio::{task::spawn_blocking, try_join};
|
||||
|
||||
use crate::{
|
||||
AppState, NEGATIVE_PREFIX, RATING_PREFIX,
|
||||
|
|
@ -40,6 +41,7 @@ use crate::{
|
|||
query::{
|
||||
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;
|
||||
|
|
@ -98,6 +100,39 @@ pub(crate) async fn logout(mut auth_session: AuthSession) -> Result<impl IntoRes
|
|||
|
||||
// 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(
|
||||
State(AppState { db, files_dir, .. }): State<AppState>,
|
||||
auth_session: AuthSession,
|
||||
|
|
@ -110,9 +145,12 @@ pub(crate) async fn upload(
|
|||
|
||||
let mut upload_tags: Option<Vec<samey_tag::Model>> = 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 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());
|
||||
|
||||
// Read multipart form data
|
||||
|
|
@ -143,73 +181,150 @@ pub(crate) async fn upload(
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
"media-file" => {
|
||||
let content_type = field
|
||||
.content_type()
|
||||
.ok_or(SameyError::Other("Missing content type".into()))?;
|
||||
let format = ImageFormat::from_mime_type(content_type).ok_or(SameyError::Other(
|
||||
format!("Unknown content type: {}", content_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(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)?;
|
||||
match Format::from_str(content_type)? {
|
||||
format @ Format::Video(video_format) => {
|
||||
media_type = Some(format.media_type());
|
||||
let thumbnail_format = ImageFormat::Png;
|
||||
let (file_name, thumbnail_file_name) = {
|
||||
let mut rng = rand::rng();
|
||||
let mut file_name: String = (0..8)
|
||||
.map(|_| rng.sample(rand::distr::Alphanumeric) as char)
|
||||
.collect();
|
||||
let thumbnail_file_name = format!(
|
||||
"thumb-{}.{}",
|
||||
file_name,
|
||||
thumbnail_format.extensions_str()[0]
|
||||
);
|
||||
file_name.push_str(video_format);
|
||||
(file_name, thumbnail_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 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,
|
||||
source_file,
|
||||
media_type,
|
||||
thumbnail_file,
|
||||
width.map(|w| w.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 {
|
||||
uploader_id: Set(user.id),
|
||||
media: Set(source_file),
|
||||
media_type: Set(media_type.into()),
|
||||
width: Set(width),
|
||||
height: Set(height),
|
||||
thumbnail: Set(thumbnail_file),
|
||||
thumbnail_width: Set(thumbnail_width),
|
||||
thumbnail_height: Set(thumbnail_height),
|
||||
title: Set(None),
|
||||
description: Set(None),
|
||||
rating: Set("u".to_owned()),
|
||||
|
|
@ -934,6 +1049,7 @@ pub(crate) async fn view_post(
|
|||
.map(|tag| &tag.name)
|
||||
.join(" "),
|
||||
rating: parent_post.rating,
|
||||
media_type: parent_post.media_type,
|
||||
}),
|
||||
None => None,
|
||||
}
|
||||
|
|
@ -960,6 +1076,7 @@ pub(crate) async fn view_post(
|
|||
.map(|tag| &tag.name)
|
||||
.join(" "),
|
||||
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)
|
||||
.join(" "),
|
||||
rating: parent_post.rating,
|
||||
media_type: parent_post.media_type,
|
||||
}),
|
||||
None => None,
|
||||
}
|
||||
|
|
@ -1263,7 +1381,7 @@ pub(crate) async fn remove_field() -> impl IntoResponse {
|
|||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "get_media.html")]
|
||||
#[template(path = "get_image_media.html")]
|
||||
struct GetMediaTemplate {
|
||||
post: samey_post::Model,
|
||||
}
|
||||
|
|
@ -1291,7 +1409,7 @@ pub(crate) async fn get_media(
|
|||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "get_full_media.html")]
|
||||
#[template(path = "get_full_image_media.html")]
|
||||
struct GetFullMediaTemplate {
|
||||
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"
|
||||
id="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>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -32,6 +32,10 @@
|
|||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
<label>Type</label>
|
||||
{{ post.media_type | capitalize }}
|
||||
</div>
|
||||
<div>
|
||||
<label>Width</label>
|
||||
{{ post.width }}px
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@
|
|||
>
|
||||
<img src="/files/{{ post.thumbnail }}" />
|
||||
<div>{{ post.rating | upper }}</div>
|
||||
<div>{{ post.media_type }}</div>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
|
|
|||
|
|
@ -6,15 +6,12 @@
|
|||
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||
<title>Post #{{ post.id }} - {{ application_name }}</title>
|
||||
<meta name="generator" content="Samey {{ env!("CARGO_PKG_VERSION") }}" />
|
||||
<meta property="og:site_name" content="{{ application_name }}" />
|
||||
<meta
|
||||
property="og:title"
|
||||
content="{% if let Some(title) = post.title %}{{ title }}{% endif %}"
|
||||
/>
|
||||
<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
|
||||
property="og:description"
|
||||
content="{% if let Some(description) = post.description %}{{ description }}{% endif %}"
|
||||
|
|
@ -23,13 +20,33 @@
|
|||
property="twitter:title"
|
||||
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" />
|
||||
{% 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>
|
||||
<body>
|
||||
<main>
|
||||
<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>
|
||||
<article>
|
||||
<h2>Details</h2>
|
||||
|
|
@ -41,6 +58,7 @@
|
|||
<a href="/post/{{ parent_post.id }}" title="{{ parent_post.tags }}">
|
||||
<img src="/files/{{ parent_post.thumbnail }}" />
|
||||
<div>{{ parent_post.rating }}</div>
|
||||
<div>{{ parent_post.media_type }}</div>
|
||||
</a>
|
||||
</article>
|
||||
{% else %}
|
||||
|
|
@ -54,6 +72,7 @@
|
|||
<a href="/post/{{ child_post.id }}" title="{{ child_post.tags }}">
|
||||
<img src="/files/{{ child_post.thumbnail }}" />
|
||||
<div>{{ child_post.rating | upper }}</div>
|
||||
<div>{{ child_post.media_type }}</div>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue