diff --git a/.sqlx/query-01a08873b7fa815ad98a56a0902b60414cfcdc2c7a8570351320c4bc425347c6.json b/.sqlx/query-01a08873b7fa815ad98a56a0902b60414cfcdc2c7a8570351320c4bc425347c6.json deleted file mode 100644 index 27ed241..0000000 --- a/.sqlx/query-01a08873b7fa815ad98a56a0902b60414cfcdc2c7a8570351320c4bc425347c6.json +++ /dev/null @@ -1,92 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path,\n r.id AS review_id, r.movie_id, r.user_id, r.rating, r.comment, r.watched_at, r.created_at\n FROM reviews r\n INNER JOIN movies m ON m.id = r.movie_id\n WHERE r.movie_id = ?\n ORDER BY r.watched_at ASC\n LIMIT ? OFFSET ?", - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "external_metadata_id", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "title", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "release_year", - "ordinal": 3, - "type_info": "Integer" - }, - { - "name": "director", - "ordinal": 4, - "type_info": "Text" - }, - { - "name": "poster_path", - "ordinal": 5, - "type_info": "Text" - }, - { - "name": "review_id", - "ordinal": 6, - "type_info": "Text" - }, - { - "name": "movie_id", - "ordinal": 7, - "type_info": "Text" - }, - { - "name": "user_id", - "ordinal": 8, - "type_info": "Text" - }, - { - "name": "rating", - "ordinal": 9, - "type_info": "Integer" - }, - { - "name": "comment", - "ordinal": 10, - "type_info": "Text" - }, - { - "name": "watched_at", - "ordinal": 11, - "type_info": "Text" - }, - { - "name": "created_at", - "ordinal": 12, - "type_info": "Text" - } - ], - "parameters": { - "Right": 3 - }, - "nullable": [ - false, - true, - false, - false, - true, - true, - false, - false, - false, - false, - true, - false, - false - ] - }, - "hash": "01a08873b7fa815ad98a56a0902b60414cfcdc2c7a8570351320c4bc425347c6" -} diff --git a/.sqlx/query-026e2afeb573707cb360fcdab8f6137aabfaf603b5ed57b98ac2888b4a0389ff.json b/.sqlx/query-026e2afeb573707cb360fcdab8f6137aabfaf603b5ed57b98ac2888b4a0389ff.json deleted file mode 100644 index 58175b3..0000000 --- a/.sqlx/query-026e2afeb573707cb360fcdab8f6137aabfaf603b5ed57b98ac2888b4a0389ff.json +++ /dev/null @@ -1,92 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path,\n r.id AS review_id, r.movie_id, r.user_id, r.rating, r.comment, r.watched_at, r.created_at\n FROM reviews r\n INNER JOIN movies m ON m.id = r.movie_id\n ORDER BY r.watched_at DESC\n LIMIT ? OFFSET ?", - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "external_metadata_id", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "title", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "release_year", - "ordinal": 3, - "type_info": "Integer" - }, - { - "name": "director", - "ordinal": 4, - "type_info": "Text" - }, - { - "name": "poster_path", - "ordinal": 5, - "type_info": "Text" - }, - { - "name": "review_id", - "ordinal": 6, - "type_info": "Text" - }, - { - "name": "movie_id", - "ordinal": 7, - "type_info": "Text" - }, - { - "name": "user_id", - "ordinal": 8, - "type_info": "Text" - }, - { - "name": "rating", - "ordinal": 9, - "type_info": "Integer" - }, - { - "name": "comment", - "ordinal": 10, - "type_info": "Text" - }, - { - "name": "watched_at", - "ordinal": 11, - "type_info": "Text" - }, - { - "name": "created_at", - "ordinal": 12, - "type_info": "Text" - } - ], - "parameters": { - "Right": 2 - }, - "nullable": [ - false, - true, - false, - false, - true, - true, - false, - false, - false, - false, - true, - false, - false - ] - }, - "hash": "026e2afeb573707cb360fcdab8f6137aabfaf603b5ed57b98ac2888b4a0389ff" -} diff --git a/.sqlx/query-0963b9661182e139cd760bbabb0d6ea3a301a2a3adbdfdda4a88f333a1144c77.json b/.sqlx/query-0963b9661182e139cd760bbabb0d6ea3a301a2a3adbdfdda4a88f333a1144c77.json deleted file mode 100644 index 2fb8300..0000000 --- a/.sqlx/query-0963b9661182e139cd760bbabb0d6ea3a301a2a3adbdfdda4a88f333a1144c77.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT COUNT(*) FROM reviews", - "describe": { - "columns": [ - { - "name": "COUNT(*)", - "ordinal": 0, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - false - ] - }, - "hash": "0963b9661182e139cd760bbabb0d6ea3a301a2a3adbdfdda4a88f333a1144c77" -} diff --git a/.sqlx/query-167481bb1692cc81531d9a5cd85425e43d09a6df97c335ac347f7cfd61acd171.json b/.sqlx/query-167481bb1692cc81531d9a5cd85425e43d09a6df97c335ac347f7cfd61acd171.json deleted file mode 100644 index 80aefc3..0000000 --- a/.sqlx/query-167481bb1692cc81531d9a5cd85425e43d09a6df97c335ac347f7cfd61acd171.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT id, email, password_hash FROM users WHERE email = ?", - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "email", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "password_hash", - "ordinal": 2, - "type_info": "Text" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false - ] - }, - "hash": "167481bb1692cc81531d9a5cd85425e43d09a6df97c335ac347f7cfd61acd171" -} diff --git a/.sqlx/query-18de90feb13b9f467f06d0ce25332d9ea7eabc99d9f1a44694e5d10762606f82.json b/.sqlx/query-18de90feb13b9f467f06d0ce25332d9ea7eabc99d9f1a44694e5d10762606f82.json deleted file mode 100644 index 78a50ea..0000000 --- a/.sqlx/query-18de90feb13b9f467f06d0ce25332d9ea7eabc99d9f1a44694e5d10762606f82.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT OR IGNORE INTO users (id, email, password_hash, created_at) VALUES (?, ?, ?, ?)", - "describe": { - "columns": [], - "parameters": { - "Right": 4 - }, - "nullable": [] - }, - "hash": "18de90feb13b9f467f06d0ce25332d9ea7eabc99d9f1a44694e5d10762606f82" -} diff --git a/.sqlx/query-3047579c6ed13ce87aad9b9ce6300c02f0df3516979518976e13f9d9abc6a403.json b/.sqlx/query-3047579c6ed13ce87aad9b9ce6300c02f0df3516979518976e13f9d9abc6a403.json deleted file mode 100644 index 54b4a4e..0000000 --- a/.sqlx/query-3047579c6ed13ce87aad9b9ce6300c02f0df3516979518976e13f9d9abc6a403.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT id, external_metadata_id, title, release_year, director, poster_path\n FROM movies WHERE title = ? AND release_year = ?", - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "external_metadata_id", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "title", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "release_year", - "ordinal": 3, - "type_info": "Integer" - }, - { - "name": "director", - "ordinal": 4, - "type_info": "Text" - }, - { - "name": "poster_path", - "ordinal": 5, - "type_info": "Text" - } - ], - "parameters": { - "Right": 2 - }, - "nullable": [ - false, - true, - false, - false, - true, - true - ] - }, - "hash": "3047579c6ed13ce87aad9b9ce6300c02f0df3516979518976e13f9d9abc6a403" -} diff --git a/.sqlx/query-33d0dae7d16b0635c1c7eb5afd10824bb55af7cc7a854f590d326622863759d1.json b/.sqlx/query-33d0dae7d16b0635c1c7eb5afd10824bb55af7cc7a854f590d326622863759d1.json deleted file mode 100644 index 5666885..0000000 --- a/.sqlx/query-33d0dae7d16b0635c1c7eb5afd10824bb55af7cc7a854f590d326622863759d1.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT id, external_metadata_id, title, release_year, director, poster_path\n FROM movies WHERE id = ?", - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "external_metadata_id", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "title", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "release_year", - "ordinal": 3, - "type_info": "Integer" - }, - { - "name": "director", - "ordinal": 4, - "type_info": "Text" - }, - { - "name": "poster_path", - "ordinal": 5, - "type_info": "Text" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - true, - false, - false, - true, - true - ] - }, - "hash": "33d0dae7d16b0635c1c7eb5afd10824bb55af7cc7a854f590d326622863759d1" -} diff --git a/.sqlx/query-47f7cf95ce3450635b643ab710cadba96f40319140834d510bc5207b2552e055.json b/.sqlx/query-47f7cf95ce3450635b643ab710cadba96f40319140834d510bc5207b2552e055.json deleted file mode 100644 index 8db3f5e..0000000 --- a/.sqlx/query-47f7cf95ce3450635b643ab710cadba96f40319140834d510bc5207b2552e055.json +++ /dev/null @@ -1,92 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path,\n r.id AS review_id, r.movie_id, r.user_id, r.rating, r.comment, r.watched_at, r.created_at\n FROM reviews r\n INNER JOIN movies m ON m.id = r.movie_id\n WHERE r.movie_id = ?\n ORDER BY r.watched_at DESC\n LIMIT ? OFFSET ?", - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "external_metadata_id", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "title", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "release_year", - "ordinal": 3, - "type_info": "Integer" - }, - { - "name": "director", - "ordinal": 4, - "type_info": "Text" - }, - { - "name": "poster_path", - "ordinal": 5, - "type_info": "Text" - }, - { - "name": "review_id", - "ordinal": 6, - "type_info": "Text" - }, - { - "name": "movie_id", - "ordinal": 7, - "type_info": "Text" - }, - { - "name": "user_id", - "ordinal": 8, - "type_info": "Text" - }, - { - "name": "rating", - "ordinal": 9, - "type_info": "Integer" - }, - { - "name": "comment", - "ordinal": 10, - "type_info": "Text" - }, - { - "name": "watched_at", - "ordinal": 11, - "type_info": "Text" - }, - { - "name": "created_at", - "ordinal": 12, - "type_info": "Text" - } - ], - "parameters": { - "Right": 3 - }, - "nullable": [ - false, - true, - false, - false, - true, - true, - false, - false, - false, - false, - true, - false, - false - ] - }, - "hash": "47f7cf95ce3450635b643ab710cadba96f40319140834d510bc5207b2552e055" -} diff --git a/.sqlx/query-4b3074b532342c6356ee0e8e4d8c4a830f016234bb690e1f6240f02824d6d84f.json b/.sqlx/query-4b3074b532342c6356ee0e8e4d8c4a830f016234bb690e1f6240f02824d6d84f.json deleted file mode 100644 index 1089c99..0000000 --- a/.sqlx/query-4b3074b532342c6356ee0e8e4d8c4a830f016234bb690e1f6240f02824d6d84f.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT COUNT(*) FROM reviews WHERE movie_id = ?", - "describe": { - "columns": [ - { - "name": "COUNT(*)", - "ordinal": 0, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false - ] - }, - "hash": "4b3074b532342c6356ee0e8e4d8c4a830f016234bb690e1f6240f02824d6d84f" -} diff --git a/.sqlx/query-630e092fcd33bc312befef352a98225e6e18e6079644b949258a39bf4b0fe3e5.json b/.sqlx/query-630e092fcd33bc312befef352a98225e6e18e6079644b949258a39bf4b0fe3e5.json deleted file mode 100644 index 8b41a06..0000000 --- a/.sqlx/query-630e092fcd33bc312befef352a98225e6e18e6079644b949258a39bf4b0fe3e5.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO reviews (id, movie_id, user_id, rating, comment, watched_at, created_at)\n VALUES (?, ?, ?, ?, ?, ?, ?)", - "describe": { - "columns": [], - "parameters": { - "Right": 7 - }, - "nullable": [] - }, - "hash": "630e092fcd33bc312befef352a98225e6e18e6079644b949258a39bf4b0fe3e5" -} diff --git a/.sqlx/query-7bc4aebcb94547976d3d7e063e4e908fc22b977b3cbf063ee93ffe4648c42011.json b/.sqlx/query-7bc4aebcb94547976d3d7e063e4e908fc22b977b3cbf063ee93ffe4648c42011.json deleted file mode 100644 index a23bd20..0000000 --- a/.sqlx/query-7bc4aebcb94547976d3d7e063e4e908fc22b977b3cbf063ee93ffe4648c42011.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT id, external_metadata_id, title, release_year, director, poster_path\n FROM movies WHERE external_metadata_id = ?", - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "external_metadata_id", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "title", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "release_year", - "ordinal": 3, - "type_info": "Integer" - }, - { - "name": "director", - "ordinal": 4, - "type_info": "Text" - }, - { - "name": "poster_path", - "ordinal": 5, - "type_info": "Text" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - true, - false, - false, - true, - true - ] - }, - "hash": "7bc4aebcb94547976d3d7e063e4e908fc22b977b3cbf063ee93ffe4648c42011" -} diff --git a/.sqlx/query-7d7e23355ee0e442f2aa27e898dcfa40bdc4b09391afe04325f076157d9d84aa.json b/.sqlx/query-7d7e23355ee0e442f2aa27e898dcfa40bdc4b09391afe04325f076157d9d84aa.json deleted file mode 100644 index 1967c47..0000000 --- a/.sqlx/query-7d7e23355ee0e442f2aa27e898dcfa40bdc4b09391afe04325f076157d9d84aa.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO movies (id, external_metadata_id, title, release_year, director, poster_path)\n VALUES (?, ?, ?, ?, ?, ?)\n ON CONFLICT(id) DO UPDATE SET\n external_metadata_id = excluded.external_metadata_id,\n title = excluded.title,\n release_year = excluded.release_year,\n director = excluded.director,\n poster_path = excluded.poster_path", - "describe": { - "columns": [], - "parameters": { - "Right": 6 - }, - "nullable": [] - }, - "hash": "7d7e23355ee0e442f2aa27e898dcfa40bdc4b09391afe04325f076157d9d84aa" -} diff --git a/.sqlx/query-af883f8b78f185077e2d3dcfaa0a6e62fbdfbf00c97c9b33b699dc631476181d.json b/.sqlx/query-af883f8b78f185077e2d3dcfaa0a6e62fbdfbf00c97c9b33b699dc631476181d.json deleted file mode 100644 index 4b94231..0000000 --- a/.sqlx/query-af883f8b78f185077e2d3dcfaa0a6e62fbdfbf00c97c9b33b699dc631476181d.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT id, movie_id, user_id, rating, comment, watched_at, created_at\n FROM reviews WHERE movie_id = ? ORDER BY watched_at ASC", - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "movie_id", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "user_id", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "rating", - "ordinal": 3, - "type_info": "Integer" - }, - { - "name": "comment", - "ordinal": 4, - "type_info": "Text" - }, - { - "name": "watched_at", - "ordinal": 5, - "type_info": "Text" - }, - { - "name": "created_at", - "ordinal": 6, - "type_info": "Text" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false, - false, - true, - false, - false - ] - }, - "hash": "af883f8b78f185077e2d3dcfaa0a6e62fbdfbf00c97c9b33b699dc631476181d" -} diff --git a/.sqlx/query-affe1eb261283c09d4b1ce6e684681755f079a044ffec8ff2bd79cfd8efe16b8.json b/.sqlx/query-affe1eb261283c09d4b1ce6e684681755f079a044ffec8ff2bd79cfd8efe16b8.json deleted file mode 100644 index 65a123c..0000000 --- a/.sqlx/query-affe1eb261283c09d4b1ce6e684681755f079a044ffec8ff2bd79cfd8efe16b8.json +++ /dev/null @@ -1,92 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path,\n r.id AS review_id, r.movie_id, r.user_id, r.rating, r.comment, r.watched_at, r.created_at\n FROM reviews r\n INNER JOIN movies m ON m.id = r.movie_id\n ORDER BY r.watched_at ASC\n LIMIT ? OFFSET ?", - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "external_metadata_id", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "title", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "release_year", - "ordinal": 3, - "type_info": "Integer" - }, - { - "name": "director", - "ordinal": 4, - "type_info": "Text" - }, - { - "name": "poster_path", - "ordinal": 5, - "type_info": "Text" - }, - { - "name": "review_id", - "ordinal": 6, - "type_info": "Text" - }, - { - "name": "movie_id", - "ordinal": 7, - "type_info": "Text" - }, - { - "name": "user_id", - "ordinal": 8, - "type_info": "Text" - }, - { - "name": "rating", - "ordinal": 9, - "type_info": "Integer" - }, - { - "name": "comment", - "ordinal": 10, - "type_info": "Text" - }, - { - "name": "watched_at", - "ordinal": 11, - "type_info": "Text" - }, - { - "name": "created_at", - "ordinal": 12, - "type_info": "Text" - } - ], - "parameters": { - "Right": 2 - }, - "nullable": [ - false, - true, - false, - false, - true, - true, - false, - false, - false, - false, - true, - false, - false - ] - }, - "hash": "affe1eb261283c09d4b1ce6e684681755f079a044ffec8ff2bd79cfd8efe16b8" -} diff --git a/Cargo.lock b/Cargo.lock index bb71a0e..740c722 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -331,6 +331,17 @@ dependencies = [ "shlex", ] +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -1245,6 +1256,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + [[package]] name = "ipnet" version = "2.12.0" @@ -1747,6 +1767,7 @@ dependencies = [ "anyhow", "async-trait", "domain", + "infer", "object_store", "tokio", "tracing", @@ -1791,6 +1812,7 @@ dependencies = [ "dotenvy", "event-publisher", "http-body-util", + "infer", "metadata", "poster-fetcher", "poster-storage", diff --git a/crates/adapters/poster-storage/Cargo.toml b/crates/adapters/poster-storage/Cargo.toml index 3b425d9..3489e74 100644 --- a/crates/adapters/poster-storage/Cargo.toml +++ b/crates/adapters/poster-storage/Cargo.toml @@ -9,6 +9,7 @@ anyhow = { workspace = true } async-trait = { workspace = true } tracing = { workspace = true } object_store = { workspace = true } +infer = "0.19.0" [dev-dependencies] tokio = { workspace = true } diff --git a/crates/adapters/poster-storage/src/lib.rs b/crates/adapters/poster-storage/src/lib.rs index 50b9008..a5f2514 100644 --- a/crates/adapters/poster-storage/src/lib.rs +++ b/crates/adapters/poster-storage/src/lib.rs @@ -7,9 +7,15 @@ use domain::{ ports::PosterStorage, value_objects::{MovieId, PosterPath}, }; -use object_store::{path::Path, ObjectStore}; +use object_store::{Attribute, Attributes, PutOptions, path::Path, ObjectStore}; use std::sync::Arc; +fn detect_mime(bytes: &[u8]) -> &'static str { + infer::get(bytes) + .map(|t| t.mime_type()) + .unwrap_or("application/octet-stream") +} + pub struct PosterStorageAdapter { store: Arc, } @@ -32,8 +38,12 @@ impl PosterStorage for PosterStorageAdapter { image_bytes: &[u8], ) -> Result { let path = Path::from(movie_id.value().to_string()); + let mime = detect_mime(image_bytes); + let mut attributes = Attributes::new(); + attributes.insert(Attribute::ContentType, mime.into()); + let opts = PutOptions { attributes, ..Default::default() }; self.store - .put(&path, image_bytes.to_vec().into()) + .put_opts(&path, image_bytes.to_vec().into(), opts) .await .map_err(|e| DomainError::InfrastructureError(e.to_string()))?; PosterPath::new(path.to_string()) diff --git a/crates/adapters/template-askama/templates/diary.html b/crates/adapters/template-askama/templates/diary.html index 00ed72c..ac0492e 100644 --- a/crates/adapters/template-askama/templates/diary.html +++ b/crates/adapters/template-askama/templates/diary.html @@ -5,7 +5,7 @@
{% if let Some(poster) = entry.movie().poster_path() %}
- +
{% endif %}
diff --git a/crates/presentation/Cargo.toml b/crates/presentation/Cargo.toml index 06ebbf4..856798d 100644 --- a/crates/presentation/Cargo.toml +++ b/crates/presentation/Cargo.toml @@ -30,6 +30,7 @@ sqlx = { workspace = true } template-askama = { workspace = true } event-publisher = { workspace = true } rss = { workspace = true } +infer = "0.19.0" [dev-dependencies] tower = { version = "0.5", features = ["util"] } diff --git a/crates/presentation/src/dtos.rs b/crates/presentation/src/dtos.rs index d9faba7..43ff60a 100644 --- a/crates/presentation/src/dtos.rs +++ b/crates/presentation/src/dtos.rs @@ -1,6 +1,19 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; +fn empty_string_as_none<'de, D, T>(de: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, + T: std::str::FromStr, + T::Err: std::fmt::Display, +{ + let s = Option::::deserialize(de)?; + match s.as_deref() { + None | Some("") => Ok(None), + Some(s) => s.parse::().map(Some).map_err(serde::de::Error::custom), + } +} + #[derive(Deserialize)] pub struct DiaryQueryParams { pub limit: Option, @@ -11,11 +24,16 @@ pub struct DiaryQueryParams { #[derive(Deserialize)] pub struct LogReviewForm { + #[serde(default, deserialize_with = "empty_string_as_none")] pub external_metadata_id: Option, + #[serde(default, deserialize_with = "empty_string_as_none")] pub manual_title: Option, + #[serde(default, deserialize_with = "empty_string_as_none")] pub manual_release_year: Option, + #[serde(default, deserialize_with = "empty_string_as_none")] pub manual_director: Option, pub rating: u8, + #[serde(default, deserialize_with = "empty_string_as_none")] pub comment: Option, pub watched_at: String, } diff --git a/crates/presentation/src/handlers.rs b/crates/presentation/src/handlers.rs index 1845002..c8b9e22 100644 --- a/crates/presentation/src/handlers.rs +++ b/crates/presentation/src/handlers.rs @@ -218,7 +218,7 @@ pub mod html { }; let cmd = LogReviewCommand { - external_metadata_id: form.external_metadata_id, + external_metadata_id: form.external_metadata_id.filter(|s| !s.trim().is_empty()), manual_title: form.manual_title, manual_release_year: form.manual_release_year, manual_director: form.manual_director, @@ -238,6 +238,37 @@ pub mod html { } } +pub mod posters { + use axum::{ + extract::{Path, State}, + http::{StatusCode, header}, + response::IntoResponse, + }; + + use domain::value_objects::PosterPath; + + use crate::state::AppState; + + pub async fn get_poster( + State(state): State, + Path(path): Path, + ) -> impl IntoResponse { + let poster_path = match PosterPath::new(path) { + Ok(p) => p, + Err(_) => return StatusCode::BAD_REQUEST.into_response(), + }; + match state.app_ctx.poster_storage.get_poster(&poster_path).await { + Ok(bytes) => { + let mime = infer::get(&bytes) + .map(|t| t.mime_type()) + .unwrap_or("application/octet-stream"); + ([(header::CONTENT_TYPE, mime)], bytes).into_response() + } + Err(_) => StatusCode::NOT_FOUND.into_response(), + } + } +} + pub mod rss { use axum::{ extract::State, @@ -360,7 +391,7 @@ pub mod api { })?; let cmd = LogReviewCommand { - external_metadata_id: req.external_metadata_id, + external_metadata_id: req.external_metadata_id.filter(|s| !s.trim().is_empty()), manual_title: req.manual_title, manual_release_year: req.manual_release_year, manual_director: req.manual_director, diff --git a/crates/presentation/src/routes.rs b/crates/presentation/src/routes.rs index 2e33807..c1a2a81 100644 --- a/crates/presentation/src/routes.rs +++ b/crates/presentation/src/routes.rs @@ -28,6 +28,7 @@ fn html_routes() -> Router { ) .route("/reviews/new", routing::get(handlers::html::get_new_review_page)) .route("/reviews", routing::post(handlers::html::post_review)) + .route("/posters/{path}", routing::get(handlers::posters::get_poster)) .route("/feed.rss", routing::get(handlers::rss::get_feed)) }