238 lines
7.5 KiB
Scala
238 lines
7.5 KiB
Scala
package lila.video
|
|
|
|
import org.joda.time.DateTime
|
|
import reactivemongo.bson._
|
|
import reactivemongo.core.commands._
|
|
import scala.concurrent.duration._
|
|
|
|
import lila.common.paginator._
|
|
import lila.db.paginator.BSONAdapter
|
|
import lila.db.Types.Coll
|
|
import lila.memo.AsyncCache
|
|
import lila.user.{ User, UserRepo }
|
|
|
|
private[video] final class VideoApi(
|
|
videoColl: Coll,
|
|
viewColl: Coll,
|
|
filterColl: Coll) {
|
|
|
|
import lila.db.BSON.BSONJodaDateTimeHandler
|
|
import reactivemongo.bson.Macros
|
|
private implicit val YoutubeBSONHandler = {
|
|
import Youtube.Metadata
|
|
Macros.handler[Metadata]
|
|
}
|
|
private implicit val VideoBSONHandler = Macros.handler[Video]
|
|
private implicit val TagNbBSONHandler = Macros.handler[TagNb]
|
|
import View.viewBSONHandler
|
|
|
|
private def videoViews(userOption: Option[User])(videos: Seq[Video]): Fu[Seq[VideoView]] = userOption match {
|
|
case None => fuccess {
|
|
videos map { VideoView(_, false) }
|
|
}
|
|
case Some(user) => view.seenVideoIds(user, videos) map { ids =>
|
|
videos.map { v =>
|
|
VideoView(v, ids contains v.id)
|
|
}
|
|
}
|
|
}
|
|
|
|
object video {
|
|
|
|
private val maxPerPage = 18
|
|
|
|
def find(id: Video.ID): Fu[Option[Video]] =
|
|
videoColl.find(BSONDocument("_id" -> id)).one[Video]
|
|
|
|
def search(user: Option[User], query: String, page: Int): Fu[Paginator[VideoView]] = {
|
|
val q = query.split(' ').map { word => s""""$word"""" } mkString " "
|
|
val textScore = BSONDocument("score" -> BSONDocument("$meta" -> "textScore"))
|
|
Paginator(
|
|
adapter = new BSONAdapter[Video](
|
|
collection = videoColl,
|
|
selector = BSONDocument(
|
|
"$text" -> BSONDocument("$search" -> q)
|
|
),
|
|
projection = textScore,
|
|
sort = textScore
|
|
) mapFutureList videoViews(user),
|
|
currentPage = page,
|
|
maxPerPage = maxPerPage)
|
|
}
|
|
|
|
def save(video: Video): Funit =
|
|
videoColl.update(
|
|
BSONDocument("_id" -> video.id),
|
|
BSONDocument("$set" -> video),
|
|
upsert = true).void
|
|
|
|
def removeNotIn(ids: List[Video.ID]) =
|
|
videoColl.remove(
|
|
BSONDocument("_id" -> BSONDocument("$nin" -> ids))
|
|
).void
|
|
|
|
def setMetadata(id: Video.ID, metadata: Youtube.Metadata) =
|
|
videoColl.update(
|
|
BSONDocument("_id" -> id),
|
|
BSONDocument("$set" -> BSONDocument("metadata" -> metadata)),
|
|
upsert = false
|
|
).void
|
|
|
|
def allIds: Fu[List[Video.ID]] =
|
|
videoColl.find(
|
|
BSONDocument(),
|
|
BSONDocument("_id" -> true)
|
|
).cursor[BSONDocument].collect[List]() map { doc =>
|
|
doc flatMap (_.getAs[String]("_id"))
|
|
}
|
|
|
|
def popular(user: Option[User], page: Int): Fu[Paginator[VideoView]] = Paginator(
|
|
adapter = new BSONAdapter[Video](
|
|
collection = videoColl,
|
|
selector = BSONDocument(),
|
|
projection = BSONDocument(),
|
|
sort = BSONDocument("metadata.likes" -> -1)
|
|
) mapFutureList videoViews(user),
|
|
currentPage = page,
|
|
maxPerPage = maxPerPage)
|
|
|
|
def byTags(user: Option[User], tags: List[Tag], page: Int): Fu[Paginator[VideoView]] =
|
|
if (tags.isEmpty) popular(user, page)
|
|
else Paginator(
|
|
adapter = new BSONAdapter[Video](
|
|
collection = videoColl,
|
|
selector = BSONDocument(
|
|
"tags" -> BSONDocument("$all" -> tags)
|
|
),
|
|
projection = BSONDocument(),
|
|
sort = BSONDocument("metadata.likes" -> -1)
|
|
) mapFutureList videoViews(user),
|
|
currentPage = page,
|
|
maxPerPage = maxPerPage)
|
|
|
|
def byAuthor(user: Option[User], author: String, page: Int): Fu[Paginator[VideoView]] =
|
|
Paginator(
|
|
adapter = new BSONAdapter[Video](
|
|
collection = videoColl,
|
|
selector = BSONDocument(
|
|
"author" -> author
|
|
),
|
|
projection = BSONDocument(),
|
|
sort = BSONDocument("metadata.likes" -> -1)
|
|
) mapFutureList videoViews(user),
|
|
currentPage = page,
|
|
maxPerPage = maxPerPage)
|
|
|
|
def similar(user: Option[User], video: Video, max: Int): Fu[Seq[VideoView]] =
|
|
videoColl.find(BSONDocument(
|
|
"tags" -> BSONDocument("$in" -> video.tags),
|
|
"_id" -> BSONDocument("$ne" -> video.id)
|
|
)).sort(BSONDocument("metadata.likes" -> -1))
|
|
.cursor[Video]
|
|
.collect[List]().map { videos =>
|
|
videos.sortBy { v => -v.similarity(video) } take max
|
|
} flatMap videoViews(user)
|
|
|
|
object count {
|
|
|
|
private val cache = AsyncCache.single(
|
|
f = videoColl.count(none),
|
|
timeToLive = 1.day)
|
|
|
|
def clearCache = cache.clear
|
|
|
|
def apply: Fu[Int] = cache apply true
|
|
}
|
|
}
|
|
|
|
object view {
|
|
|
|
def find(videoId: Video.ID, userId: String): Fu[Option[View]] =
|
|
viewColl.find(BSONDocument(
|
|
View.BSONFields.id -> View.makeId(videoId, userId)
|
|
)).one[View]
|
|
|
|
def add(a: View) = (viewColl insert a).void recover {
|
|
case e: reactivemongo.core.commands.LastError if e.getMessage.contains("duplicate key error") => ()
|
|
}
|
|
|
|
def hasSeen(user: User, video: Video): Fu[Boolean] =
|
|
viewColl.count(BSONDocument(
|
|
View.BSONFields.id -> View.makeId(video.id, user.id)
|
|
).some) map (0!=)
|
|
|
|
def seenVideoIds(user: User, videos: Seq[Video]): Fu[Set[Video.ID]] =
|
|
viewColl.find(
|
|
BSONDocument(
|
|
"_id" -> BSONDocument("$in" -> videos.map { v =>
|
|
View.makeId(v.id, user.id)
|
|
})
|
|
),
|
|
BSONDocument(View.BSONFields.videoId -> true, "_id" -> false)
|
|
).cursor[BSONDocument].collect[List]() map { docs =>
|
|
docs.flatMap(_.getAs[String](View.BSONFields.videoId)).toSet
|
|
}
|
|
}
|
|
|
|
object tag {
|
|
|
|
def paths(filterTags: List[Tag]): Fu[List[TagNb]] = pathsCache(filterTags.sorted)
|
|
|
|
def allPopular: Fu[List[TagNb]] = popularCache(true)
|
|
|
|
def clearCache = pathsCache.clear >> popularCache.clear
|
|
|
|
private val max = 25
|
|
|
|
private val pathsCache = AsyncCache[List[Tag], List[TagNb]](
|
|
f = filterTags => {
|
|
val allPaths =
|
|
if (filterTags.isEmpty) allPopular map { tags =>
|
|
tags.filterNot(_.isNumeric)
|
|
}
|
|
else {
|
|
val command = Aggregate(videoColl.name, Seq(
|
|
Match(BSONDocument("tags" -> BSONDocument("$all" -> filterTags))),
|
|
Project("tags" -> BSONBoolean(true)),
|
|
Unwind("tags"),
|
|
GroupField("tags")("nb" -> SumValue(1))
|
|
))
|
|
videoColl.db.command(command) map {
|
|
_.toList.flatMap(_.asOpt[TagNb])
|
|
}
|
|
}
|
|
allPopular zip allPaths map {
|
|
case (all, paths) =>
|
|
val tags = all map { t =>
|
|
paths find (_._id == t._id) getOrElse TagNb(t._id, 0)
|
|
} filterNot (_.empty) take max
|
|
val missing = filterTags filterNot { t =>
|
|
tags exists (_.tag == t)
|
|
}
|
|
val list = tags.take(max - missing.size) ::: missing.flatMap { t =>
|
|
all find (_.tag == t)
|
|
}
|
|
list.sortBy { t =>
|
|
if (filterTags contains t.tag) Int.MinValue
|
|
else -t.nb
|
|
}
|
|
}
|
|
},
|
|
timeToLive = 1.day)
|
|
|
|
private val popularCache = AsyncCache.single[List[TagNb]](
|
|
f = {
|
|
val command = Aggregate(videoColl.name, Seq(
|
|
Project("tags" -> BSONBoolean(true)),
|
|
Unwind("tags"),
|
|
GroupField("tags")("nb" -> SumValue(1)),
|
|
Sort(Seq(Descending("nb")))
|
|
))
|
|
videoColl.db.command(command) map {
|
|
_.toList.flatMap(_.asOpt[TagNb])
|
|
}
|
|
},
|
|
timeToLive = 1.day)
|
|
}
|
|
}
|