album/crates/album-core/models.rs
Andrey Tkachenko 8d03abb434
All checks were successful
continuous-integration/drone/push Build is passing
Update README.md
2023-07-07 17:23:35 +04:00

437 lines
11 KiB
Rust

use crate::schema::attributions::dsl as a;
use crate::schema::cameras;
use crate::schema::cameras::dsl as c;
use crate::schema::people::dsl as h;
use crate::schema::photo_people::dsl as ph;
use crate::schema::photo_places::dsl as pl;
use crate::schema::photo_tags::dsl as pt;
use crate::schema::photos;
use crate::schema::photos::dsl as p;
use crate::schema::places::dsl as l;
use crate::schema::positions::dsl as pos;
use crate::schema::tags::dsl as t;
use async_trait::async_trait;
use chrono::naive::NaiveDateTime;
use diesel::pg::Pg;
use diesel::prelude::*;
use diesel::result::Error;
use diesel::sql_types::Integer;
use diesel_async::{AsyncPgConnection, RunQueryDsl};
use slug::slugify;
use std::cmp::max;
pub struct PhotoDetails {
photo: Photo,
pub people: Vec<Person>,
pub places: Vec<Place>,
pub tags: Vec<Tag>,
pub pos: Option<Coord>,
pub attribution: Option<String>,
pub camera: Option<Camera>,
}
impl PhotoDetails {
pub async fn load(
id: i32,
db: &mut AsyncPgConnection,
) -> Result<Self, Error> {
use crate::schema::photos::dsl::photos;
let photo = photos.find(id).first::<Photo>(db).await?;
let attribution = if let Some(id) = photo.attribution_id {
Some(a::attributions.find(id).select(a::name).first(db).await?)
} else {
None
};
let camera = if let Some(id) = photo.camera_id {
Some(c::cameras.find(id).first(db).await?)
} else {
None
};
Ok(PhotoDetails {
photo,
people: h::people
.filter(
h::id.eq_any(
ph::photo_people
.select(ph::person_id)
.filter(ph::photo_id.eq(id)),
),
)
.load(db)
.await?,
places: l::places
.filter(
l::id.eq_any(
pl::photo_places
.select(pl::place_id)
.filter(pl::photo_id.eq(id)),
),
)
.order(l::osm_level.desc().nulls_first())
.load(db)
.await?,
tags: t::tags
.filter(
t::id.eq_any(
pt::photo_tags
.select(pt::tag_id)
.filter(pt::photo_id.eq(id)),
),
)
.load(db)
.await?,
pos: pos::positions
.filter(pos::photo_id.eq(id))
.select((pos::latitude, pos::longitude))
.first(db)
.await
.optional()?,
attribution,
camera,
})
}
}
impl std::ops::Deref for PhotoDetails {
type Target = Photo;
fn deref(&self) -> &Photo {
&self.photo
}
}
#[derive(AsChangeset, Clone, Debug, Identifiable, Queryable, Selectable)]
pub struct Photo {
pub id: i32,
pub path: String,
pub date: Option<NaiveDateTime>,
pub grade: Option<i16>,
pub rotation: i16,
pub is_public: bool,
pub camera_id: Option<i32>,
pub attribution_id: Option<i32>,
pub width: i32,
pub height: i32,
}
#[derive(Debug)]
pub enum Modification<T> {
Created(T),
Updated(T),
Unchanged(T),
}
impl Photo {
#[allow(dead_code)]
pub fn is_public(&self) -> bool {
self.is_public
}
pub fn cache_key(&self, size: SizeTag) -> String {
format!("rp{}{:?}", self.id, size)
}
#[allow(dead_code)]
pub fn query<'a>(auth: bool) -> photos::BoxedQuery<'a, Pg> {
let result = p::photos
.filter(p::path.not_like("%.CR2"))
.filter(p::path.not_like("%.dng"))
.into_boxed();
if auth {
result
} else {
result.filter(p::is_public)
}
}
pub async fn update_by_path(
db: &mut AsyncPgConnection,
file_path: &str,
newwidth: i32,
newheight: i32,
exifdate: Option<NaiveDateTime>,
camera: &Option<Camera>,
) -> Result<Option<Modification<Photo>>, Error> {
if let Some(mut pic) = p::photos
.filter(p::path.eq(&file_path.to_string()))
.first::<Photo>(db)
.await
.optional()?
{
let mut change = false;
// TODO Merge updates to one update statement!
if pic.width != newwidth || pic.height != newheight {
change = true;
pic = diesel::update(p::photos.find(pic.id))
.set((p::width.eq(newwidth), p::height.eq(newheight)))
.get_result::<Photo>(db)
.await?;
}
if exifdate.is_some() && exifdate != pic.date {
change = true;
pic = diesel::update(p::photos.find(pic.id))
.set(p::date.eq(exifdate))
.get_result::<Photo>(db)
.await?;
}
if let Some(ref camera) = *camera {
if pic.camera_id != Some(camera.id) {
change = true;
pic = diesel::update(p::photos.find(pic.id))
.set(p::camera_id.eq(camera.id))
.get_result::<Photo>(db)
.await?;
}
}
Ok(Some(if change {
Modification::Updated(pic)
} else {
Modification::Unchanged(pic)
}))
} else {
Ok(None)
}
}
pub async fn create_or_set_basics(
db: &mut AsyncPgConnection,
file_path: &str,
newwidth: i32,
newheight: i32,
exifdate: Option<NaiveDateTime>,
exifrotation: i16,
camera: Option<Camera>,
) -> Result<Modification<Photo>, Error> {
if let Some(result) = Self::update_by_path(
db, file_path, newwidth, newheight, exifdate, &camera,
)
.await?
{
Ok(result)
} else {
let pic = diesel::insert_into(p::photos)
.values((
p::path.eq(file_path),
p::date.eq(exifdate),
p::rotation.eq(exifrotation),
p::width.eq(newwidth),
p::height.eq(newheight),
p::camera_id.eq(camera.map(|c| c.id)),
))
.get_result::<Photo>(db)
.await?;
Ok(Modification::Created(pic))
}
}
pub fn get_size(&self, size: SizeTag) -> (u32, u32) {
let (width, height) = (self.width, self.height);
let scale = f64::from(size.px()) / f64::from(max(width, height));
let w = (scale * f64::from(width)) as u32;
let h = (scale * f64::from(height)) as u32;
match self.rotation {
_x @ (0..=44 | 315..=360 | 135..=224) => (w, h),
_ => (h, w),
}
}
#[cfg(test)]
pub fn mock(y: i32, mo: u32, da: u32, h: u32, m: u32, s: u32) -> Self {
use chrono::naive::NaiveDate;
Photo {
id: ((((((y as u32 * 12) + mo) * 30 + da) * 24) + h) * 60 + s)
as i32,
path: format!("{y}/{mo:02}/{da:02}/IMG{h:02}{m:02}{s:02}.jpg"),
date: NaiveDate::from_ymd_opt(y, mo, da)
.unwrap()
.and_hms_opt(h, m, s),
grade: None,
rotation: 0,
is_public: false,
camera_id: None,
attribution_id: None,
width: 4000,
height: 3000,
}
}
}
#[async_trait]
pub trait Facet {
async fn load_slugs(
slugs: &[String],
db: &mut AsyncPgConnection,
) -> Result<Vec<Self>, Error>
where
Self: Sized;
}
#[derive(Debug, Clone, Queryable)]
pub struct Tag {
pub id: i32,
pub slug: String,
pub tag_name: String,
}
#[async_trait]
impl Facet for Tag {
async fn load_slugs(
slugs: &[String],
db: &mut AsyncPgConnection,
) -> Result<Vec<Tag>, Error> {
t::tags.filter(t::slug.eq_any(slugs)).load(db).await
}
}
#[derive(Debug, Clone, Queryable)]
pub struct PhotoTag {
pub id: i32,
pub photo_id: i32,
pub tag_id: i32,
}
#[derive(Debug, Clone, Queryable)]
pub struct Person {
pub id: i32,
pub slug: String,
pub person_name: String,
}
impl Person {
pub async fn get_or_create_name(
db: &mut AsyncPgConnection,
name: &str,
) -> Result<Person, Error> {
if let Some(name) = h::people
.filter(h::person_name.ilike(name))
.first(db)
.await
.optional()?
{
Ok(name)
} else {
diesel::insert_into(h::people)
.values((h::person_name.eq(name), h::slug.eq(&slugify(name))))
.get_result(db)
.await
}
}
}
#[async_trait]
impl Facet for Person {
async fn load_slugs(
slugs: &[String],
db: &mut AsyncPgConnection,
) -> Result<Vec<Person>, Error> {
h::people.filter(h::slug.eq_any(slugs)).load(db).await
}
}
#[derive(Debug, Clone, Queryable)]
pub struct PhotoPerson {
pub photo_id: i32,
pub person_id: i32,
}
#[derive(Debug, Clone, Queryable)]
pub struct Place {
pub id: i32,
pub slug: String,
pub place_name: String,
pub osm_id: Option<i64>,
pub osm_level: Option<i16>,
}
#[async_trait]
impl Facet for Place {
async fn load_slugs(
slugs: &[String],
db: &mut AsyncPgConnection,
) -> Result<Vec<Place>, Error> {
l::places.filter(l::slug.eq_any(slugs)).load(db).await
}
}
#[derive(Debug, Clone, Queryable)]
pub struct PhotoPlace {
pub photo_id: i32,
pub place_id: i32,
}
#[derive(Debug, Clone, Identifiable, Queryable)]
pub struct Camera {
pub id: i32,
pub manufacturer: String,
pub model: String,
}
impl Camera {
pub async fn get_or_create(
db: &mut AsyncPgConnection,
make: &str,
modl: &str,
) -> Result<Camera, Error> {
if let Some(camera) = c::cameras
.filter(c::manufacturer.eq(make))
.filter(c::model.eq(modl))
.first::<Camera>(db)
.await
.optional()?
{
Ok(camera)
} else {
diesel::insert_into(c::cameras)
.values((c::manufacturer.eq(make), c::model.eq(modl)))
.get_result(db)
.await
}
}
}
#[derive(Debug, Clone)]
pub struct Coord {
pub x: f64,
pub y: f64,
}
impl Queryable<(Integer, Integer), Pg> for Coord {
type Row = (i32, i32);
fn build(row: Self::Row) -> diesel::deserialize::Result<Self> {
Ok(Coord::from((row.0, row.1)))
}
}
impl From<(i32, i32)> for Coord {
fn from((lat, long): (i32, i32)) -> Coord {
Coord {
x: f64::from(lat) / 1e6,
y: f64::from(long) / 1e6,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SizeTag {
Small,
Medium,
Large,
}
impl SizeTag {
pub fn px(self) -> u16 {
match self {
SizeTag::Small => 288,
SizeTag::Medium => 1080,
SizeTag::Large => 8192, // not really used
}
}
pub fn tag(self) -> char {
match self {
SizeTag::Small => 's',
SizeTag::Medium => 'm',
SizeTag::Large => 'l',
}
}
}