implement local image features

This commit is contained in:
yggverse 2026-01-10 17:53:48 +02:00
parent 3e94399ccb
commit e86b241ee6
6 changed files with 52 additions and 21 deletions

View file

@ -171,7 +171,10 @@ fn crawl(tx: &mut mysql::Transaction, channel_config: &config::Channel) -> Resul
} }
}; };
let content_image_id = tx.insert_content_image(content_id, image_id)?; let content_image_id = tx.insert_content_image(content_id, image_id)?;
debug!("Add content image relationship #{content_image_id}") debug!("Add content image relationship #{content_image_id}");
let uri = format!("/image/{image_id}");
tx.replace_content_description(content_id, src, &uri)?;
debug!("Replace content image in description from `{src}` to `{uri}`")
} }
} }
} }

View file

@ -1,10 +1,6 @@
title = "rssto" title = "rssto"
#description = "" #description = ""
# Replace image sources with local
# * if crawled with the `persist_images_selector` selector
local_images = true
format_time = "%d/%m/%Y %H:%M" format_time = "%d/%m/%Y %H:%M"
# Provider ID (`provider` table) # Provider ID (`provider` table)

View file

@ -12,7 +12,12 @@ use feed::Feed;
use global::Global; use global::Global;
use meta::Meta; use meta::Meta;
use mysql::{Database, table::Sort}; use mysql::{Database, table::Sort};
use rocket::{State, http::Status, response::content::RawXml, serde::Serialize}; use rocket::{
State,
http::{ContentType, Status},
response::content::RawXml,
serde::Serialize,
};
use rocket_dyn_templates::{Template, context}; use rocket_dyn_templates::{Template, context};
#[get("/?<search>&<page>")] #[get("/?<search>&<page>")]
@ -25,9 +30,8 @@ fn index(
) -> Result<Template, Status> { ) -> Result<Template, Status> {
#[derive(Serialize)] #[derive(Serialize)]
#[serde(crate = "rocket::serde")] #[serde(crate = "rocket::serde")]
struct Content { struct Row {
content_id: u64, content_id: u64,
description: String,
link: String, link: String,
time: String, time: String,
title: String, title: String,
@ -81,15 +85,14 @@ fn index(
.into_iter() .into_iter()
.map(|content| { .map(|content| {
let channel_item = conn.channel_item(content.channel_item_id).unwrap().unwrap(); let channel_item = conn.channel_item(content.channel_item_id).unwrap().unwrap();
Content { Row {
content_id: content.content_id, content_id: content.content_id,
description: content.description,
link: channel_item.link, link: channel_item.link,
time: time(channel_item.pub_date).format(&global.format_time).to_string(), time: time(channel_item.pub_date).format(&global.format_time).to_string(),
title: content.title, title: content.title,
} }
}) })
.collect::<Vec<Content>>(), .collect::<Vec<Row>>(),
page: page.unwrap_or(1), page: page.unwrap_or(1),
pages: (total as f64 / global.list_limit as f64).ceil(), pages: (total as f64 / global.list_limit as f64).ceil(),
total, total,
@ -140,6 +143,21 @@ fn info(
} }
} }
#[get("/image/<image_id>")]
fn image(image_id: u64, db: &State<Database>) -> Result<(ContentType, Vec<u8>), Status> {
let mut conn = db.connection().map_err(|e| {
error!("Could not connect database: `{e}`");
Status::InternalServerError
})?;
match conn.image(image_id).map_err(|e| {
error!("Could not get content image `{image_id}`: `{e}`");
Status::InternalServerError
})? {
Some(image) => Ok((ContentType::Bytes, image.data)),
None => Err(Status::NotFound),
}
}
#[get("/rss?<search>")] #[get("/rss?<search>")]
fn rss( fn rss(
search: Option<&str>, search: Option<&str>,
@ -221,7 +239,7 @@ fn rocket() -> _ {
title: config.title, title: config.title,
version: env!("CARGO_PKG_VERSION").into(), version: env!("CARGO_PKG_VERSION").into(),
}) })
.mount("/", routes![index, rss, info]) .mount("/", routes![index, rss, info, image])
} }
const S: &str = ""; const S: &str = "";

View file

@ -5,10 +5,7 @@
<div> <div>
<a name="{{ row.content_id }}"></a> <a name="{{ row.content_id }}"></a>
<h2><a href="{{ row.content_id }}">{{ row.title }}</a></h2> <h2><a href="{{ row.content_id }}">{{ row.title }}</a></h2>
{% if row.time %}<p>{{ row.time }}</p>{% endif %} {{ row.time }}
<div>
{{ row.description | safe }}
</div>
</div> </div>
{% endfor %} {% endfor %}
{% else %} {% else %}

View file

@ -80,11 +80,15 @@ impl Connection {
) )
} }
pub fn images(&mut self, limit: Option<usize>) -> Result<Vec<Image>, Error> { pub fn image(&mut self, image_id: u64) -> Result<Option<Image>, Error> {
self.conn.query(format!( self.conn.exec_first(
"SELECT `image_id`, `source`, `data` FROM `image` LIMIT {}", "SELECT `image_id`,
limit.unwrap_or(DEFAULT_LIMIT) `sha256`,
)) `src`,
`url`,
`data` FROM `image` WHERE `image_id` = ?",
(image_id,),
)
} }
pub fn provider_id_by_name(&mut self, name: &str) -> Result<Option<u64>, Error> { pub fn provider_id_by_name(&mut self, name: &str) -> Result<Option<u64>, Error> {

View file

@ -107,6 +107,19 @@ impl Transaction {
Ok(self.tx.last_insert_id().unwrap()) Ok(self.tx.last_insert_id().unwrap())
} }
pub fn replace_content_description(
&mut self,
content_id: u64,
from: &str,
to: &str,
) -> Result<(), Error> {
self.tx.exec_drop(
"UPDATE `content` SET `description` = REPLACE(`description`, ?, ?)
WHERE`content_id` = ?",
(from, to, content_id),
)
}
pub fn insert_content_image(&mut self, content_id: u64, image_id: u64) -> Result<u64, Error> { pub fn insert_content_image(&mut self, content_id: u64, image_id: u64) -> Result<u64, Error> {
self.tx.exec_drop( self.tx.exec_drop(
"INSERT INTO `content_image` SET `content_id` = ?, `image_id` = ?", "INSERT INTO `content_image` SET `content_id` = ?, `image_id` = ?",