Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Image analysis job #307

Merged
merged 21 commits into from May 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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