Skip to content

Commit

Permalink
Add API for changing video/series metadata
Browse files Browse the repository at this point in the history
This adds a somewhat generic function that can be used
to send `update metadata` put requests to Opencast for
both events and series, as their respective endpoints
only differ in one string (i.e. `series` vs `events`).

For now, the function is only used to update title and
description, but it can be used for any kind of dublincore
metadata.
  • Loading branch information
owi92 committed Feb 5, 2025
1 parent 36d16f6 commit 7d96095
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 10 deletions.
4 changes: 4 additions & 0 deletions backend/src/api/model/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,10 @@ impl OcEndpoint for AuthorizedEvent {
"events"
}

fn metadata_flavor(&self) -> &'static str {
"dublincore/episode"
}

async fn extra_roles(&self, context: &Context, oc_id: &str) -> Result<Vec<AclInput>> {
let query = "\
select unnest(preview_roles) as role, 'preview' as action from events where opencast_id = $1
Expand Down
63 changes: 62 additions & 1 deletion backend/src/api/model/series.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ use crate::{
util::{impl_from_db, select},
},
model::{ExtraMetadata, Key},
prelude::*, sync::client::{AclInput, OcEndpoint},
prelude::*,
sync::client::{AclInput, OcEndpoint},
};

use self::acl::AclInputEntry;
Expand Down Expand Up @@ -360,6 +361,62 @@ impl Series {
Err(err::opencast_error!("Opencast API error: {}", response.status()))
}
}

pub(crate) async fn update_metadata(
id: Id,
title: &str,
description: Option<&str>,
context: &Context,
) -> ApiResult<Series> {
let series = Self::load_by_id(id, context)
.await?
.ok_or_else(|| invalid_input!("`seriesId` does not refer to a valid series"))?;

info!(series_id = %id, "Requesting metadata update of series");

let metadata = serde_json::json!([
{
"id": "title",
"value": title
},
{
"id": "description",
"value": description
},
]);

let response = context
.oc_client
.update_metadata(&series, &series.opencast_id, metadata)
.await
.map_err(|e| {
error!("Failed to send metadata update request: {}", e);
err::opencast_unavailable!("Failed to send metadata update request")
})?;

if response.status() == StatusCode::OK {
// 200: The series' metadata has been updated.
context.db.execute("\
update series \
set title = $2, description = $3 \
where id = $1 \
", &[&series.key, &title, &description]).await?;

Self::load_by_id(id, context)
.await?
.ok_or_else(|| err::invalid_input!(
key = "series.metadata.not-found",
"series not found",
))
} else {
warn!(
series_id = %id,
"Failed to update series metadata, OC returned status: {}",
response.status(),
);
Err(err::opencast_error!("Opencast API error: {}", response.status()))
}
}
}

/// Represents an Opencast series.
Expand Down Expand Up @@ -447,6 +504,10 @@ impl OcEndpoint for Series {
"series"
}

fn metadata_flavor(&self) -> &'static str {
"dublincore/series"
}

async fn extra_roles(&self, _context: &Context, _oc_id: &str) -> Result<Vec<AclInput>> {
// Series do not have custom or preview roles.
Ok(vec![])
Expand Down
11 changes: 11 additions & 0 deletions backend/src/api/mutation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,17 @@ impl Mutation {
Series::update_acl(id, acl, context).await
}

// Updates the title and description of a series. A request for this is sent to Opencast,
// and the series is preliminarily updated in Tobira's DB.
async fn update_series_metadata(
id: Id,
title: String,
description: Option<String>,
context: &Context,
) -> ApiResult<Series> {
Series::update_metadata(id, &title, description.as_deref(), context).await
}

/// Sets the order of all children of a specific realm.
///
/// `childIndices` must contain at least one element, i.e. do not call this
Expand Down
42 changes: 33 additions & 9 deletions backend/src/sync/client.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::time::{Duration, Instant};

use bytes::Bytes;
use chrono::{DateTime, Utc, TimeZone};
use chrono::{DateTime, TimeZone, Utc};
use form_urlencoded::Serializer;
use hyper::{
Response, Request, StatusCode,
Expand Down Expand Up @@ -158,8 +158,7 @@ impl OcClient {
acl: &[AclInputEntry],
context: &Context,
) -> Result<Response<Incoming>> {
let endpoint_name = endpoint.endpoint_name();
let pq = format!("/api/{endpoint_name}/{oc_id}/acl", );
let pq = format!("/api/{}/{oc_id}/acl", endpoint.endpoint_name());
let mut access_policy = Vec::new();

// Temporary solution to add custom and preview roles
Expand All @@ -180,13 +179,27 @@ impl OcClient {
let params = Serializer::new(String::new())
.append_pair("acl", &serde_json::to_string(&access_policy).expect("Failed to serialize"))
.finish();
let req = self.authed_req_builder(&self.external_api_node, &pq)
.method(http::Method::PUT)
.header(http::header::CONTENT_TYPE, "application/x-www-form-urlencoded")
.body(params.into())
.expect("failed to build request");

self.http_client.request(req).await.map_err(Into::into)
self.send_put_request(&pq, params).await
}

pub async fn update_metadata<T: OcEndpoint>(
&self,
endpoint: &T,
oc_id: &str,
metadata: serde_json::Value,
) -> Result<Response<Incoming>> {
let pq = format!(
"/api/{}/{oc_id}/metadata?type={}",
endpoint.endpoint_name(),
endpoint.metadata_flavor(),
);

let params = Serializer::new(String::new())
.append_pair("metadata", &serde_json::to_string(&metadata).expect("Failed to serialize"))
.finish();

self.send_put_request(&pq, params).await
}

pub async fn start_workflow(&self, oc_id: &str, workflow_id: &str) -> Result<Response<Incoming>> {
Expand Down Expand Up @@ -217,6 +230,16 @@ impl OcClient {
Ok(out.processing_state == "RUNNING")
}

async fn send_put_request(&self, path_and_query: &str, params: String) -> Result<Response<Incoming>> {
let req = self.authed_req_builder(&self.external_api_node, &path_and_query)
.method(http::Method::PUT)
.header(http::header::CONTENT_TYPE, "application/x-www-form-urlencoded")
.body(params.into())
.expect("failed to build request");

self.http_client.request(req).await.map_err(Into::into)
}

fn build_authed_req(&self, node: &HttpHost, path_and_query: &str) -> (Uri, Request<RequestBody>) {
let req = self.authed_req_builder(node, path_and_query)
.body(RequestBody::empty())
Expand Down Expand Up @@ -294,5 +317,6 @@ pub struct EventStatus {

pub(crate) trait OcEndpoint {
fn endpoint_name(&self) -> &'static str;
fn metadata_flavor(&self) -> &'static str;
async fn extra_roles(&self, context: &Context, oc_id: &str) -> Result<Vec<AclInput>>;
}
2 changes: 2 additions & 0 deletions frontend/src/i18n/locales/de.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,8 @@ api-remote-errors:
acl:
not-found: "Zugriffsrechte konnten nicht geändert werden: Serie nicht gefunden."
not-allowed: Sie haben nicht die Berechtigung, die Zugriffsreche dieser Serie zu ändern.
metadata:
not-found: "Metadaten konnten nicht geändert werden: Serie nicht gefunden."

embed:
not-supported: Diese Seite kann nicht eingebettet werden.
2 changes: 2 additions & 0 deletions frontend/src/i18n/locales/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,8 @@ api-remote-errors:
acl:
not-found: "Access policy update failed: series not found."
not-allowed: You are not allowed to update the access policies of this series.
metadata:
not-found: "Metadata update failed: series not found."

embed:
not-supported: This page can't be embedded.
1 change: 1 addition & 0 deletions frontend/src/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,7 @@ type Mutation {
this means it might get overwritten again if the update in Opencast failed for some reason.
"""
updateSeriesAcl(id: ID!, acl: [AclInputEntry!]!): Series!
updateSeriesMetadata(id: ID!, title: String!, description: String): Series!
"""
Sets the order of all children of a specific realm.
Expand Down

0 comments on commit 7d96095

Please sign in to comment.