video library WIP

pull/382/head
Thibault Duplessis 2015-03-22 17:52:26 +01:00
parent 0f9f3c8f8f
commit da16a6d9ed
13 changed files with 248 additions and 34 deletions

View File

@ -7,24 +7,46 @@ import play.twirl.api.Html
import lila.api.Context
import lila.app._
import lila.common.HTTPRequest
import lila.video.{ View, Video => VideoModel }
import lila.video.{ View, Video => VideoModel, UserControl }
import views._
object Video extends LilaController {
private def env = Env.video
private def UserControl[A](f: UserControl => Fu[A])(implicit ctx: Context): Fu[A] =
env.api.userControl(ctx.userId | "anon", 25) flatMap f
private def renderIndex(control: UserControl)(implicit ctx: Context) =
env.api.video.byTags(control.filter.tags, 15) map { videos =>
Ok(html.video.index(videos, control))
}
def index = Open { implicit ctx =>
env.api.video.popular(9) map { videos =>
html.video.index(videos)
UserControl { control =>
val filter = control.filter.copy(
tags = get("tags") ?? (_.split(',').toList.map(_.trim.toLowerCase))
)
env.api.filter.set(filter) >>
renderIndex(control.copy(filter = filter))
}
}
def show(id: String) = Open { implicit ctx =>
env.api.video.find(id) flatMap {
case None => fuccess(html.video.notFound())
case Some(video) => env.api.video.similar(video, 6) map { similar =>
html.video.show(video, similar)
UserControl { control =>
env.api.video.find(id) flatMap {
case None => fuccess(NotFound(html.video.notFound(control)))
case Some(video) => env.api.video.similar(video, 9) map { similar =>
Ok(html.video.show(video, similar, control))
}
}
}
}
def author(author: String) = Open { implicit ctx =>
UserControl { control =>
env.api.video byAuthor author map { videos =>
Ok(html.video.author(author, videos, control))
}
}
}

View File

@ -0,0 +1,18 @@
@(author: String, videos: List[lila.video.Video], control: lila.video.UserControl)(implicit ctx: Context)
@layout(
title = s"$author • Free Chess Videos",
control = control) {
<div class="content_box_top">
<a class="is4 text lichess_title" data-icon="i" href="@routes.Video.index"></a>
<h1 class="lichess_title">
@author • @pluralize("video", videos.size) found
</h1>
</div>
<div class="list">
@videos.map { video =>
@card(video)
}
</div>
}

View File

@ -1,18 +1,33 @@
@(videos: List[lila.video.Video])(implicit ctx: Context)
@side = {
}
@(videos: List[lila.video.Video], control: lila.video.UserControl)(implicit ctx: Context)
@layout(
title = "Free Chess Videos",
side = side.some) {
control = control) {
<div class="content_box_top">
<h1 data-icon="@openingBrace" class="is4 text lichess_title">Video library</h1>
<h1 data-icon="@openingBrace" class="is4 text lichess_title">
@if(control.filter.tags.nonEmpty) {
@pluralize("video", videos.size) found
} else {
Video library
}
</h1>
</div>
<div class="list">
@videos.map { video =>
@card(video)
}
@if(videos.size < 4) {
<div class="not_much nb_@videos.size">
@if(videos.isEmpty) {
No videos for these tags!
} else {
That's all we got for these tags!
}
<br />
<br />
<a href="@routes.Video.index" class="button">Clear search</a>
</div>
}
</div>
}

View File

@ -1,8 +1,16 @@
@(title: String, side: Option[Html] = None)(body: Html)(implicit ctx: Context)
@(title: String, control: lila.video.UserControl)(body: Html)(implicit ctx: Context)
@sideSection = {
<div class="side">
@side
<div id="video_side">
<div class="tag_list">
@control.tags.map { t =>
<a class="@if(control.filter.tags contains t.tag) {checked}"
href="@routes.Video.index?tags=@control.filter.toggle(t.tag).tagString">
<em>@t.nb</em>
@t.tag.capitalize
</a>
}
</div>
</div>
}

View File

@ -1,14 +1,16 @@
@()(implicit ctx: Context)
@(control: lila.video.UserControl)(implicit ctx: Context)
@layout(title = "Video not found") {
@layout(
title = "Video not found",
control = control) {
<div class="content_box_top">
<h1 data-icon="@openingBrace" class="is4 text lichess_title">
<a href="@routes.Video.index">Video library</a>
</h1>
<a class="is4 text lichess_title" data-icon="i" href="@routes.Video.index">Video library</a>
</div>
<div class="not_found">
<h2>Video Not Found!</h2>
<a href="@routes.Video.index">View all videos</a>
<h1>Video Not Found!</h1>
<br />
<br />
<a class="big button text" data-icon="i" href="@routes.Video.index">Return to the video library</a>
</div>
}

View File

@ -1,14 +1,19 @@
@(video: lila.video.Video, similar: List[lila.video.Video])(implicit ctx: Context)
@(video: lila.video.Video, similar: List[lila.video.Video], control: lila.video.UserControl)(implicit ctx: Context)
@layout(title = s"${video.title} • Free Chess Videos") {
@layout(
title = s"${video.title} • Free Chess Videos",
control = control) {
<div class="show">
<div class="content_box_top">
<a class="is4 text lichess_title" data-icon="i" href="@routes.Video.index">Video library</a>
</div>
<iframe id="ytplayer" type="text/html" width="792" height="482"
src="http://www.youtube.com/embed/@video.id?autoplay=0&origin=http://lichess.org"
src="http://www.youtube.com/embed/@video.id?autoplay=1&origin=http://lichess.org"
frameborder="0"></iframe>
<h1 class="title">@video.title</h1>
<div class="meta">
<a class="author" href="#">@video.author</a>
<a class="author" href="@routes.Video.author(video.author)">@video.author</a>
@video.tags.map { tag =>
<a class="tag" data-icon="o" href="#">@tag.capitalize</a>
}

View File

@ -0,0 +1,3 @@
@(tags: List[String])(implicit ctx: Context)

View File

@ -183,6 +183,7 @@ GET /setup/validate-fen controllers.Setup.validateFen
# Video
GET /video controllers.Video.index
GET /video/author/:author controllers.Video.author(author: String)
GET /video/:id controllers.Video.show(id: String)
# I18n

View File

@ -14,6 +14,7 @@ final class Env(
private val settings = new {
val CollectionVideo = config getString "collection.video"
val CollectionView = config getString "collection.view"
val CollectionFilter = config getString "collection.filter"
val SheetUrl = config getString "sheet.url"
val SheetDelay = config duration "sheet.delay"
val YoutubeUrl = config getString "youtube.url"
@ -25,7 +26,8 @@ final class Env(
lazy val api = new VideoApi(
videoColl = videoColl,
viewColl = viewColl)
viewColl = viewColl,
filterColl = filterColl)
private lazy val sheet = new Sheet(
url = SheetUrl,
@ -46,11 +48,12 @@ final class Env(
}
scheduler.once(10 seconds) {
sheet.fetchAll >> youtube.updateAll logFailure "video boot"
// sheet.fetchAll >> youtube.updateAll logFailure "video boot"
}
private[video] lazy val videoColl = db(CollectionVideo)
private[video] lazy val viewColl = db(CollectionView)
private[video] lazy val filterColl = db(CollectionFilter)
}
object Env {

View File

@ -0,0 +1,28 @@
package lila.video
import org.joda.time.DateTime
case class TagNb(_id: Tag, nb: Int) {
def tag = _id
def noSpace = tag.replace(" ", "-")
}
case class Filter(
_id: String, // user ID
tags: List[String],
date: DateTime) {
def id = _id
def toggle(tag: String) = copy(
tags = if (tags contains tag) tags filter (tag!=) else tags :+ tag
)
def tagString = tags mkString ","
}
case class UserControl(
filter: Filter,
tags: List[TagNb])

View File

@ -3,13 +3,16 @@ package lila.video
import org.joda.time.DateTime
import reactivemongo.bson._
import reactivemongo.core.commands._
import scala.concurrent.duration._
import spray.caching.{ LruCache, Cache }
import lila.db.Types.Coll
import lila.user.{ User, UserRepo }
private[video] final class VideoApi(
videoColl: Coll,
viewColl: Coll) {
viewColl: Coll,
filterColl: Coll) {
import lila.db.BSON.BSONJodaDateTimeHandler
import reactivemongo.bson.Macros
@ -18,6 +21,8 @@ private[video] final class VideoApi(
Macros.handler[Metadata]
}
private implicit val VideoBSONHandler = Macros.handler[Video]
private implicit val TagNbBSONHandler = Macros.handler[TagNb]
private implicit val FilterBSONHandler = Macros.handler[Filter]
import View.viewBSONHandler
object video {
@ -28,7 +33,7 @@ private[video] final class VideoApi(
def save(video: Video): Funit =
videoColl.update(
BSONDocument("_id" -> video.id),
video,
BSONDocument("$set" -> video),
upsert = true).void
def removeNotIn(ids: List[Video.ID]) =
@ -57,12 +62,28 @@ private[video] final class VideoApi(
.cursor[Video]
.collect[List](max)
def byTags(tags: List[Tag], max: Int): Fu[List[Video]] =
if (tags.isEmpty) popular(max)
else videoColl.find(BSONDocument(
"tags" -> BSONDocument("$all" -> tags)
)).sort(BSONDocument(
"metadata.likes" -> -1
)).cursor[Video]
.collect[List]()
def byAuthor(author: String): Fu[List[Video]] =
videoColl.find(BSONDocument(
"author" -> author
)).sort(BSONDocument(
"metadata.likes" -> -1
)).cursor[Video]
.collect[List]()
def similar(video: Video, max: Int): Fu[List[Video]] =
videoColl.find(BSONDocument(
"tags" -> BSONDocument("$in" -> video.tags),
"_id" -> BSONDocument("$ne" -> video.id)
))
.sort(BSONDocument("metadata.likes" -> -1))
)).sort(BSONDocument("metadata.likes" -> -1))
.cursor[Video]
.collect[List]().map { videos =>
videos.sortBy { v => -v.similarity(video) } take max
@ -83,4 +104,44 @@ private[video] final class VideoApi(
View.BSONFields.id -> View.makeId(video.id, user.id)
).some) map (0!=)
}
object tag {
private val cache: Cache[List[TagNb]] = LruCache(timeToLive = 1.day)
def clearCache = fuccess(cache.clear)
def popular(max: Int): Fu[List[TagNb]] = cache(max) {
import reactivemongo.core.commands._
val command = Aggregate(videoColl.name, Seq(
Project("tags" -> BSONBoolean(true)),
Unwind("tags"),
GroupField("tags")("nb" -> SumValue(1)),
Sort(Seq(Descending("nb"))),
Limit(max)
))
videoColl.db.command(command) map {
_.toList.flatMap(_.asOpt[TagNb])
}
}
}
object filter {
def get(userId: String) =
filterColl.find(
BSONDocument("_id" -> userId)
).one[Filter] map (_ | Filter(userId, Nil, DateTime.now))
def set(filter: Filter) =
filterColl.update(
BSONDocument("_id" -> filter.id),
BSONDocument("$set" -> filter.copy(
date = DateTime.now
)),
upsert = true).void
}
def userControl(userId: String, maxTags: Int): Fu[UserControl] =
filter.get(userId) zip tag.popular(maxTags) map (UserControl.apply _).tupled
}

View File

@ -91,6 +91,9 @@ time {
[data-icon].is4::before {
font-size: 30px;
}
.is4.text::before {
margin-right: 10px;
}
[data-icon].is-green::before {
color: #759900;
}

View File

@ -1,3 +1,6 @@
#video {
min-height: 823px;
}
#video a {
text-decoration: none;
}
@ -5,6 +8,9 @@
padding-bottom: 10px;
border-bottom: 1px solid #ccc;
}
#video .content_box_top .lichess_title {
display: inline;
}
#video .list {
overflow: hidden;
padding: 8px 0 8px 8px;
@ -73,7 +79,6 @@
#video .card .tags span {
display: block;
}
#video .show .meta {
padding: 0 25px 10px 25px;
}
@ -94,3 +99,43 @@
#video .show .tag:hover {
opacity: 1;
}
#video_side {
margin-top: 20px;
margin-left: -30px;
width: 230px;
}
#video_side .tag_list {
text-transform: capitalize;
}
#video_side .tag_list a {
display: block;
padding: 5px;
text-decoration: none;
border-left: 2px solid transparent;
transition: 0.13s;
}
#video_side .tag_list a:hover {
background: #f8f8f8;
border-color: #ccc;
}
#video_side .tag_list em {
float: right;
font-weight: bold;
opacity: 0.7;
}
#video_side .tag_list a.checked {
background: #fff;
border-color: #d85000;
}
#video .not_found {
margin-top: 200px;
text-align: center;
}
#video .not_much {
margin-top: 100px;
text-align: center;
}
#video .not_much.nb_0 {
margin-top: 200px;
}