Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import scala.collection.{JavaConverters, Map}
import scala.concurrent.{ExecutionContext, Future}
import com.fasterxml.jackson.databind.ObjectMapper
import org.sunbird.content.upload.mgr.validator.ArtifactUrlValidator
import scala.sys.process._

class ContentActor @Inject() (implicit oec: OntologyEngineContext, ss: StorageService) extends BaseActor {

Expand Down Expand Up @@ -84,9 +85,165 @@ class ContentActor @Inject() (implicit oec: OntologyEngineContext, ss: StorageSe
case "createMLContent" => createMLContent(request)
case "reviewMLContent" => reviewMLContent(request)
case "updateReviewStatusMLContent" => updateReviewStatusMLContent(request)
case "getVideoDuration" => getVideoDuration(request)
case _ => ERROR(request.getOperation)
}
}
def getVideoDuration(request: Request): Future[Response] = {
val resourceDoId = request.getRequest.getOrDefault("resourceDoId", "").asInstanceOf[String]
val contentDoId = request.getRequest.getOrDefault("contentDoId", "").asInstanceOf[String]
val videoUrl = request.getRequest.getOrDefault("videoUrl", "").asInstanceOf[String]
if (StringUtils.isBlank(resourceDoId) || StringUtils.isBlank(contentDoId) || StringUtils.isBlank(videoUrl)) {
throw new ClientException("ERR_INVALID_REQUEST", "resourceDoId, contentDoId and videoUrl are mandatory")
}
// ---------------- RESOURCE READ ----------------
val resourceReadReq = new Request()
resourceReadReq.setContext(new java.util.HashMap[String, AnyRef]() {{
put("graph_id", "domain")
put("version", "1.0")
put("objectType", "Content")
put("schemaName", "content")
}}
)
resourceReadReq.put("identifier", resourceDoId)
resourceReadReq.put("mode", "read")
resourceReadReq.put("fields", new java.util.ArrayList[String]())
// ---------------- CONTENT READ ----------------
val contentReadReq = new Request()
contentReadReq.setContext(
new java.util.HashMap[String, AnyRef]() {{
put("graph_id", "domain")
put("version", "1.0")
put("objectType", "Content")
put("schemaName", "content")
}}
)
contentReadReq.put("identifier", contentDoId)
contentReadReq.put("mode", "read")
contentReadReq.put("fields", new java.util.ArrayList[String]())

def toIntDuration(value: String): Int = {
try {
if (StringUtils.isBlank(value)) 0 else value.toDouble.toInt
} catch {
case _: Exception => 0
}
}

def buildAbsoluteVideoUrl(inputVideoUrl: String): String = {
val normalized = StringUtils.stripStart(inputVideoUrl, "/")
val withoutContentStoreHost = StringUtils.removeStartIgnoreCase(normalized, "https://portal.dev.karmayogibharat.net/content-store/")
val withoutHost = StringUtils.removeStartIgnoreCase(withoutContentStoreHost, "https://portal.dev.karmayogibharat.net/")
val sanitizedInput = StringUtils.stripStart(withoutHost, "/")
"https://portal.dev.karmayogibharat.net/content-store/" + sanitizedInput
}

def buildStorageKey(inputVideoUrl: String): String = {
val withoutHost = StringUtils.removeStartIgnoreCase(inputVideoUrl, "https://portal.dev.karmayogibharat.net/")
StringUtils.removeStart(StringUtils.stripStart(withoutHost, "/"), "content-store/")
}

def buildUpdateRequest(identifier: String, updatePayload: java.util.Map[String, AnyRef]): Request = {
val updateReq = new Request()
updateReq.setObjectType("Content")
updateReq.setContext(new java.util.HashMap[String, AnyRef]() {{
put("graph_id", "domain")
put("version", "1.0")
put("objectType", "Content")
put("schemaName", "content")
put("identifier", identifier)
put("skipValidation", Boolean.box(true))
}})
updateReq.setOperation("systemUpdate")
updateReq.setRequest(updatePayload)
updateReq
}

val readNodesF = for {
resourceNodeOpt <- DataNode.read(resourceReadReq).map(Option(_)).recover {
case ex: Exception =>
logger.warn(s"Unable to read resourceDoId=$resourceDoId. Falling back duration=0", ex)
None
}
contentNodeOpt <- DataNode.read(contentReadReq).map(Option(_)).recover {
case ex: Exception =>
logger.warn(s"Unable to read contentDoId=$contentDoId. Falling back duration=0", ex)
None
}
} yield (resourceNodeOpt, contentNodeOpt)

readNodesF.flatMap { case (resourceNodeOpt, contentNodeOpt) =>
// existing durations and version keys from DB
val resourceOldDuration = resourceNodeOpt.flatMap(node => Option(node.getMetadata.get("duration"))).map(_.toString).getOrElse("0")
val resourceVersionKey = resourceNodeOpt.flatMap(node => Option(node.getMetadata.get("versionKey"))).map(_.toString).getOrElse("")
val contentOldDuration = contentNodeOpt.flatMap(node => Option(node.getMetadata.get("duration"))).map(_.toString).getOrElse("0")
val contentVersionKey =
contentNodeOpt.flatMap(node => Option(node.getMetadata.get("versionKey"))).map(_.toString).getOrElse("")

// latest duration from video URL

val absoluteVideoUrl = buildAbsoluteVideoUrl(videoUrl)
logger.info(s"getVideoDuration absoluteVideoUrl=$absoluteVideoUrl")
val storageKey = buildStorageKey(videoUrl)
val latestVideoDuration =
try {

val command = Seq(
"ffprobe",
"-v", "error",
"-show_entries", "format=duration",
"-of", "default=noprint_wrappers=1:nokey=1",
absoluteVideoUrl
)
val output = command.!!.trim
output.toDouble.toInt.toString
} catch {
case ex: Exception =>
logger.error("Error while fetching duration", ex)
"0"
}

val resourceOldDurationInt = toIntDuration(resourceOldDuration)
val contentOldDurationInt = toIntDuration(contentOldDuration)
val latestVideoDurationInt = toIntDuration(latestVideoDuration)
val updatedContentDurationInt = Math.max(0, contentOldDurationInt - resourceOldDurationInt + latestVideoDurationInt)
val updatedContentDuration = updatedContentDurationInt.toString

val resourceUpdatePayload = new java.util.HashMap[String, AnyRef]() {{
put("versionKey", resourceVersionKey)
put("identifier", resourceDoId)
put("previewUrl", absoluteVideoUrl)
put("artifactUrl", absoluteVideoUrl)
put("cloudStorageKey", storageKey)
put("s3Key", storageKey)
put("duration", latestVideoDuration)
}}

val contentUpdatePayload = new java.util.HashMap[String, AnyRef]() {{
put("versionKey", contentVersionKey)
put("identifier", contentDoId)
put("duration", updatedContentDuration)
}}

val resourceUpdateReq = buildUpdateRequest(resourceDoId, resourceUpdatePayload)
val contentUpdateReq = buildUpdateRequest(contentDoId, contentUpdatePayload)

for {
_ <- systemUpdate(resourceUpdateReq)
_ <- systemUpdate(contentUpdateReq)
} yield {
ResponseHandler.OK
.put("resourceOldDuration", resourceOldDuration)
.put("resourceVersionKey", resourceVersionKey)
.put("contentOldDuration", contentOldDuration)
.put("contentVersionKey", contentVersionKey)
.put("latestVideoDuration", latestVideoDuration)
.put("contentUpdatedDuration", updatedContentDuration)
.put("resourceUpdateStatus", "success")
.put("contentUpdateStatus", "success")
}
}
}

def create(request: Request): Future[Response] = {
populateDefaultersForCreation(request)
Expand Down Expand Up @@ -991,4 +1148,4 @@ class ContentActor @Inject() (implicit oec: OntologyEngineContext, ss: StorageSe
}


}
}
Original file line number Diff line number Diff line change
Expand Up @@ -249,5 +249,15 @@ class ContentController @Inject()(@Named(ActorNames.CONTENT_ACTOR) contentActor:
getResult(ApiId.UPDATE_REVIEW_STATUS_ML_CONTENT, contentActor, contentRequest, version = apiVersion)
}

def getVideoDuration() = Action.async { implicit request =>
val headers = commonHeaders()
val body = requestBody()
val content = body.getOrDefault(schemaName, new java.util.HashMap()).asInstanceOf[java.util.Map[String, Object]]
content.putAll(headers)
val contentRequest = getRequest(content, headers, "getVideoDuration")
setRequestContext(contentRequest, version, objectType, schemaName)
getResult(ApiId.UPDATE_CONTENT_DURATION, contentActor, contentRequest, version = apiVersion)
}


}
}
4 changes: 3 additions & 1 deletion content-api/content-service/app/utils/ApiId.scala
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,6 @@ object ApiId {
val RETIREMENT_STATUS_V1 = "api.content.v1.retirement.status"

val EXTENDED_READ_CONTENT = "api.extended.content.read"
}

val UPDATE_CONTENT_DURATION = "api.update.content.duration"
}
1 change: 1 addition & 0 deletions content-api/content-service/conf/routes
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ GET /content/v1/retirement/validate/:identifier controllers.v4.ExtendedConte
POST /content/v1/retirement/request/decide controllers.v4.ExtendedContentController.decideRetirementRequest
POST /content/v1/retirement/status controllers.v4.ExtendedContentController.getRetirementStatus
GET /content/v1/extended/read/:identifier controllers.v4.ExtendedContentController.extendedRead(identifier:String, mode:Option[String], fields:Option[String])
POST /content/v4/video/duration controllers.v4.ContentController.getVideoDuration
# App v4 APIs
POST /app/v4/register controllers.v4.AppController.register
PATCH /app/v4/update/:identifier controllers.v4.AppController.update(identifier:String)
Expand Down