Skip to content

Commit d39ee27

Browse files
committed
json api
1 parent 247284c commit d39ee27

File tree

4 files changed

+95
-7
lines changed

4 files changed

+95
-7
lines changed

build.sbt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,13 @@ lazy val commonSettings = Seq(
3535
lazy val backendSettings = Seq(
3636
name := "webm-tv",
3737
libraryDependencies ++= {
38-
val akkaV = "2.4.6"
38+
val akkaV = "2.4.10"
3939
Seq(
4040
"org.jsoup" % "jsoup" % "1.9.2",
4141
"com.typesafe.akka" %% "akka-actor" % akkaV,
4242
"com.typesafe.akka" %% "akka-http-experimental" % akkaV,
4343
"com.lihaoyi" %% "scalatags" % "0.5.4",
44-
"com.lihaoyi" %% "upickle" % "0.3.8",
44+
"com.lihaoyi" %% "upickle" % "0.4.1",
4545
"net.databinder.dispatch" %% "dispatch-core" % "0.11.2",
4646
"org.scala-lang.modules" %% "scala-async" % "0.9.6-RC2",
4747
"org.scalatest" %% "scalatest" % "2.2.4" % "test",

src/main/resources/reference.conf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ webm-tv {
66
thread-ttl = 1h
77

88
sosach {
9+
host = "2ch.hk"
910
boards = [
1011
b
1112
]

src/main/scala/com/karasiq/webmtv/app/Main.scala

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import java.util.concurrent.TimeUnit
55
import akka.actor._
66
import akka.http.scaladsl.Http
77
import akka.http.scaladsl.Http.ServerBinding
8-
import akka.stream.{ActorMaterializer, ActorMaterializerSettings}
8+
import akka.stream.ActorMaterializer
99
import akka.util.Timeout
10-
import com.karasiq.webmtv.sosach.M2chBoardApi
10+
import com.karasiq.webmtv.sosach.{BoardApi, Json2chBoardApi, M2chBoardApi}
1111
import com.typesafe.config.ConfigFactory
1212

1313
import scala.concurrent.Await
@@ -21,12 +21,23 @@ object Main extends App {
2121

2222
implicit val actorSystem = ActorSystem("webm-tv")
2323
implicit val executionContext = actorSystem.dispatcher
24-
implicit val actorMaterializer = ActorMaterializer(ActorMaterializerSettings(actorSystem))
24+
implicit val actorMaterializer = ActorMaterializer()
2525

26-
val boardApi = new M2chBoardApi()
26+
val config = ConfigFactory.load().getConfig("webm-tv")
27+
val boardApi = new BoardApi {
28+
val jsonApi = new Json2chBoardApi(config.getString("sosach.host"))
29+
val htmlApi = new M2chBoardApi() // Fallback
30+
31+
def board(name: String) = {
32+
jsonApi.board(name).recoverWithRetries(1, { case _ htmlApi.board(name) })
33+
}
34+
35+
def thread(board: String, id: Long) = {
36+
jsonApi.thread(board, id).recoverWithRetries(1, { case _ htmlApi.thread(board, id) })
37+
}
38+
}
2739
val store = WebmFileStore
2840
val storeDispatcher = actorSystem.actorOf(Props(classOf[WebmStoreDispatcher], boardApi, store), "storeDispatcher")
29-
val config = ConfigFactory.load().getConfig("webm-tv")
3041
val server = new Server(storeDispatcher)
3142

3243
Http().bindAndHandle(server.route, config.getString("host"), config.getInt("port")).onComplete {
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package com.karasiq.webmtv.sosach
2+
3+
import akka.actor.ActorSystem
4+
import akka.http.scaladsl.Http
5+
import akka.http.scaladsl.model.headers.Accept
6+
import akka.http.scaladsl.model.{HttpRequest, MediaRange, MediaTypes}
7+
import akka.stream.ActorMaterializer
8+
import akka.stream.scaladsl.Source
9+
import akka.util.ByteString
10+
import derive.key
11+
import upickle.Js
12+
import upickle.default._
13+
14+
private object JsonApiObjects {
15+
case class PostId(value: Long) extends AnyVal
16+
object PostId {
17+
implicit def postIdToLong(postId: PostId): Long = postId.value
18+
}
19+
20+
case class File(path: String)
21+
case class Post(@key("num") id: PostId, subject: String, comment: String, files: Seq[File])
22+
case class Thread(posts: Seq[Post])
23+
case class Board(@key("Board") name: String, pages: Seq[Int], threads: Seq[Thread])
24+
case class ThreadWrapped(@key("Board") board: String, @key("threads") thread: Option[Thread])
25+
26+
object Implicits {
27+
implicit val postIdReader = Reader[PostId] {
28+
case Js.Str(str)
29+
PostId(str.toLong)
30+
31+
case Js.Num(num)
32+
PostId(num.toLong)
33+
}
34+
}
35+
}
36+
37+
class Json2chBoardApi(host: String = "2ch.hk")(implicit as: ActorSystem, am: ActorMaterializer) extends BoardApi {
38+
import JsonApiObjects.Implicits._
39+
40+
private val http = Http()
41+
42+
private def retrieveJson[T: Reader](url: String) = {
43+
Source
44+
.fromFuture(http.singleRequest(HttpRequest(uri = url, headers = List(Accept(MediaRange(MediaTypes.`application/json`))))))
45+
.flatMapConcat(_.entity.dataBytes)
46+
.fold(ByteString.empty)(_ ++ _)
47+
.map(bs read[T](bs.utf8String))
48+
}
49+
50+
private def jsonToAppPost(board: String, postObj: JsonApiObjects.Post): Board.Post = {
51+
Board.Post(postObj.id, postObj.subject, postObj.comment, postObj.files.map(file s"https://$host/$board/${file.path}"))
52+
}
53+
54+
def board(name: String) = {
55+
val url = s"https://$host/$name/index.json"
56+
retrieveJson[JsonApiObjects.Board](url)
57+
.flatMapConcat { page
58+
val pages = page.pages.filter(_ > 0).map(page s"https://$host/$name/$page.json")
59+
Source.single(page).concat(
60+
Source(pages.toVector)
61+
.flatMapConcat(url retrieveJson[JsonApiObjects.Board](url).recoverWithRetries(1, { case _ Source.empty }))
62+
)
63+
}
64+
.mapConcat { board
65+
val threads = for (threadObj board.threads)
66+
yield Board.Thread(board.name, threadObj.posts.map(jsonToAppPost(board.name, _)))
67+
threads.toVector
68+
}
69+
}
70+
71+
def thread(board: String, id: Long) = {
72+
val url = s"https://$host/$board/res/$id.json"
73+
retrieveJson[JsonApiObjects.ThreadWrapped](url)
74+
.map(thread Board.Thread(thread.board, thread.thread.toSeq.flatMap(_.posts.map(jsonToAppPost(thread.board, _)))))
75+
}
76+
}

0 commit comments

Comments
 (0)