Skip to content

Commit

Permalink
✨ Image analysis job (#307)
Browse files Browse the repository at this point in the history
* Progress toward image analysis job

* Get functional test of analyze image.

* Finish first draft of job.

* Fix lint errors

* Clean up code and add doc comments

* Add media analysis api paths and series management button.

* Fix accidentally removed error result.

* Reorganize client API

* Address comments

* Remove errant tracing log in analyze_media_job

* Update images_analyzed count during task

* Only load ids when building list for analysis job.

* Add media_updated state and reactivate epub

* Enforce permissions

* Fix error in rar page counting and address todo

* Fix utopia descriptions.

* Update metadata properly, address comments

* Update page count if it is None
  • Loading branch information
JMicheli committed May 13, 2024
1 parent 707b9d0 commit 8a8bd86
Show file tree
Hide file tree
Showing 21 changed files with 526 additions and 39 deletions.
37 changes: 37 additions & 0 deletions apps/server/src/routers/api/v1/library.rs
Expand Up @@ -25,6 +25,7 @@ use stump_core::{
PrismaCountTrait,
},
filesystem::{
analyze_media_job::{AnalyzeMediaJob, AnalyzeMediaJobVariant},
get_unknown_thumnail,
image::{
self, generate_thumbnail, place_thumbnail, remove_thumbnails,
Expand Down Expand Up @@ -96,6 +97,7 @@ pub(crate) fn mount(app_state: AppState) -> Router<AppState> {
.route("/clean", put(clean_library))
.route("/series", get(get_library_series))
.route("/media", get(get_library_media))
.route("/analyze", post(start_media_analysis))
.nest(
"/thumbnail",
Router::new()
Expand Down Expand Up @@ -1726,3 +1728,38 @@ async fn get_library_stats(

Ok(Json(stats))
}

#[utoipa::path(
post,
path = "/api/v1/libraries/:id/analyze",
tag = "library",
params(
("id" = String, Path, description = "The ID of the library to analyze")
),
responses(
(status = 200, description = "Successfully started library media analysis"),
(status = 401, description = "Unauthorized"),
(status = 403, description = "Forbidden"),
(status = 404, description = "Library not found"),
(status = 500, description = "Internal server error"),
)
)]
async fn start_media_analysis(
Path(id): Path<String>,
State(ctx): State<AppState>,
session: Session,
) -> APIResult<()> {
let _ = enforce_session_permissions(&session, &[UserPermission::ManageLibrary])?;

// Start analysis job
ctx.enqueue_job(AnalyzeMediaJob::new(
AnalyzeMediaJobVariant::AnalyzeLibrary(id),
))
.map_err(|e| {
let err = "Failed to enqueue analyze library media job";
error!(?e, err);
APIError::InternalServerError(err.to_string())
})?;

APIResult::Ok(())
}
54 changes: 46 additions & 8 deletions apps/server/src/routers/api/v1/media.rs
Expand Up @@ -3,7 +3,7 @@ use std::path::PathBuf;
use axum::{
extract::{DefaultBodyLimit, Multipart, Path, State},
middleware::from_extractor_with_state,
routing::{get, put},
routing::{get, post, put},
Json, Router,
};
use axum_extra::extract::Query;
Expand All @@ -23,6 +23,7 @@ use stump_core::{
CountQueryReturn,
},
filesystem::{
analyze_media_job::{AnalyzeMediaJob, AnalyzeMediaJobVariant},
get_unknown_thumnail,
image::{
generate_thumbnail, place_thumbnail, remove_thumbnails, ImageFormat,
Expand All @@ -38,6 +39,7 @@ use stump_core::{
},
};
use tower_sessions::Session;
use tracing::error;
use utoipa::ToSchema;

use crate::{
Expand Down Expand Up @@ -81,6 +83,7 @@ pub(crate) fn mount(app_state: AppState) -> Router<AppState> {
// TODO: configurable max file size
.layer(DefaultBodyLimit::max(20 * 1024 * 1024)), // 20MB
)
.route("/analyze", post(start_media_analysis))
.route("/page/:page", get(get_media_page))
.route(
"/progress",
Expand Down Expand Up @@ -1194,13 +1197,13 @@ async fn replace_media_thumbnail(

// Note: I chose to *safely* attempt the removal as to not block the upload, however after some
// user testing I'd like to see if this becomes a problem. We'll see!
match remove_thumbnails(&[book_id.clone()], ctx.config.get_thumbnails_dir()) {
Ok(count) => tracing::info!("Removed {} thumbnails!", count),
Err(e) => tracing::error!(
?e,
"Failed to remove existing media thumbnail before replacing!"
),
}
remove_thumbnails(&[book_id.clone()], ctx.config.get_thumbnails_dir())
.unwrap_or_else(|e| {
tracing::error!(
?e,
"Failed to remove existing media thumbnail before replacing!"
);
});

let path_buf = place_thumbnail(&book_id, ext, &bytes, &ctx.config)?;

Expand Down Expand Up @@ -1550,3 +1553,38 @@ async fn put_media_complete_status(
completed_at: updated_or_created_rp.completed_at.map(|ca| ca.to_rfc3339()),
}))
}

#[utoipa::path(
post,
path = "/api/v1/media/:id/analyze",
tag = "media",
params(
("id" = String, Path, description = "The ID of the media to analyze")
),
responses(
(status = 200, description = "Successfully started media analysis"),
(status = 401, description = "Unauthorized"),
(status = 403, description = "Forbidden"),
(status = 404, description = "Media not found"),
(status = 500, description = "Internal server error"),
)
)]
async fn start_media_analysis(
Path(id): Path<String>,
State(ctx): State<AppState>,
session: Session,
) -> APIResult<()> {
let _ = enforce_session_permissions(&session, &[UserPermission::ManageLibrary])?;

// Start analysis job
ctx.enqueue_job(AnalyzeMediaJob::new(
AnalyzeMediaJobVariant::AnalyzeSingleItem(id),
))
.map_err(|e| {
let err = "Failed to enqueue analyze media job";
error!(?e, err);
APIError::InternalServerError(err.to_string())
})?;

APIResult::Ok(())
}
37 changes: 37 additions & 0 deletions apps/server/src/routers/api/v1/series.rs
Expand Up @@ -19,6 +19,7 @@ use stump_core::{
PrismaCountTrait, SeriesDAO, DAO,
},
filesystem::{
analyze_media_job::{AnalyzeMediaJob, AnalyzeMediaJobVariant},
get_unknown_thumnail,
image::{
generate_thumbnail, place_thumbnail, remove_thumbnails, ImageFormat,
Expand Down Expand Up @@ -74,6 +75,7 @@ pub(crate) fn mount(app_state: AppState) -> Router<AppState> {
.route("/", get(get_series_by_id))
.route("/scan", post(scan_series))
.route("/media", get(get_series_media))
.route("/analyze", post(start_media_analysis))
.route("/media/next", get(get_next_in_series))
.route(
"/thumbnail",
Expand Down Expand Up @@ -1059,3 +1061,38 @@ async fn get_series_is_complete(
async fn put_series_is_complete() -> APIResult<Json<SeriesIsComplete>> {
Err(APIError::NotImplemented)
}

#[utoipa::path(
post,
path = "/api/v1/series/:id/analyze",
tag = "series",
params(
("id" = String, Path, description = "The ID of the series to analyze")
),
responses(
(status = 200, description = "Successfully started series media analysis"),
(status = 401, description = "Unauthorized"),
(status = 403, description = "Forbidden"),
(status = 404, description = "Series not found"),
(status = 500, description = "Internal server error"),
)
)]
async fn start_media_analysis(
Path(id): Path<String>,
State(ctx): State<AppState>,
session: Session,
) -> APIResult<()> {
let _ = enforce_session_permissions(&session, &[UserPermission::ManageLibrary])?;

// Start analysis job
ctx.enqueue_job(AnalyzeMediaJob::new(AnalyzeMediaJobVariant::AnalyzeSeries(
id,
)))
.map_err(|e| {
let err = "Failed to enqueue analyze series media job";
error!(?e, err);
APIError::InternalServerError(err.to_string())
})?;

APIResult::Ok(())
}
1 change: 1 addition & 0 deletions core/src/filesystem/image/thumbnail/generation_job.rs
Expand Up @@ -149,6 +149,7 @@ impl JobExt for ThumbnailGenerationJob {
ThumbnailGenerationJobVariant::MediaGroup(media_ids) => media_ids.clone(),
};

// TODO Should find a way to keep the same ThumbnailManager around for the whole job execution
let manager = ThumbnailManager::new(ctx.config.clone())
.map_err(|e| JobError::TaskFailed(e.to_string()))?;

Expand Down
1 change: 0 additions & 1 deletion core/src/filesystem/image/thumbnail/mod.rs
Expand Up @@ -68,7 +68,6 @@ pub fn generate_thumbnail(
Ok(thumbnail_path)
}

// TODO: does this need to return a result?
pub fn generate_thumbnails(
media: &[Media],
options: ImageProcessorOptions,
Expand Down

0 comments on commit 8a8bd86

Please sign in to comment.