はじめに
Scala, Play Framework, Akka Actor, Slick を利用し、LGTM画像を驚くほど簡単につくれるWebサービスを作った話です。
LGTM とは
Looks Good To Me の略です。
GitHubのプルリクエストをレビューした時、「私は問題ないと思います」という意味で「LGTM」と書きこみます。
LGTM画像とは
「LGTM」と4文字コメントするだけではそっけないので、Looks Good な画像を付けてコメントすることがあります。
こういった画像を探すのに便利な http://lgtm.in/ というサービスがあります。(現在は閉鎖?されてしまったようです)
しかし、LGTM.in はあくまでも単なる画像共有サービスです。
LGTM.inに自分の気に入った画像がないので作りたい!もうワンランクの上のLGTM画像が欲しい!
でも画像を用意して文字を重ねて...とかするのは面倒だ!というのが僕の思いでした。
そこで、「なんかいい感じのLGTM画像を驚くほど簡単につくれる」Webサービスを作りました。
デモ
使い方
- 一番上の検索窓に、キーワードを入力します。
- 画像検索が走るので、LGTM画像のベースにしたい画像をクリックします。
- しばらく待つか、画面をリロードするとLGTM画像ができあがります。
- 出来たLGTM画像をクリックすると画像のURLやGitHubのMarkdownがコピーできるので、プルリクのコメントなどに貼り付けます。
より高度な使い方
- 一番上の検索窓に、LGTM画像のベースにしたい画像のURLを入力します。
- しばらく待つか、画面をリロードするとLGTM画像ができあがります。
- 以下同様です。
注意点
-
herokuの無料プランですので、動作が重かったり、容量がいっぱいになってLGTM画像が生成できなくなったりします。優しく扱ってください。有料プランにしました。 - どこかから怒られそうな画像は使わないようにしてください。
- ソースコードは公開しています。
技術的な話
アプリケーション構成
アプリケーション構成は以下の通りです。
フロントでの画像表示はvue.jsを使っています。採用理由は、Reactよりシンプルに書けて、個人開発でのメンテナンスがしやすいからです。
画像生成に ImageMagick を、DBアクセスには Scala のライブラリである Slick を使っています。DBにはherokuのPostgreSQLを利用しています。
画像生成フロー
フロー図は以下の通りです。ベースとなる画像のダウンロードと、LGTM画像の生成には時間がかかるので、Actorを使って非同期に処理しています。画像生成APIが叩かれると、Actorに画像生成メッセージを送り、即座にAPIレスポンスを返します。
vue.js(フロント)
フロントの画像表示部分にはvue.js ( https://jp.vuejs.org/ ) を使っています。vue.jsは、Reactより学習コストや実装コストが低く、小規模な個人開発向けです。時間がないのでなるべく手抜きをしています。
Scala/Play(バックエンド)
バックエンドの言語はScalaです。ScalaのWebフレームワークであるPlay Frameworkを使っています。ScalaのWebフレームワークの中では圧倒的に人気で、ScalaでWebアプリを作る場合、普通はPlayを選択するのではないかな、と思います。
Akka Actor
画像のダウンロードや加工には時間がかかります。画像生成APIが叩かれた際に、レスポンスはすぐに返し、時間のかかる処理は裏で進めたいです。その際に役に立つのが Akka Actor です。
今回のアプリケーションでは、「画像を生成しろ」というメッセージを送ると、Actorが裏で画像のダウンロードと加工を行ってくれます。
まず、「画像を生成しろ」メッセージを定義します。
case class ImageGenerateMessage(id: Long, url: String)
idは、DBの情報を更新するために必要な情報、urlはダウンロードすべきベース画像のURLです。
Actorは以下のようになっています。
class ImageActor extends Actor {
val downloadDir = "/tmp"
val convertedDir = "/tmp"
val imageStorage = ImageStorage
val imageRepository = ImageRepository
val imageMagickService = ImageMagickService
/** メッセージを受け取った時の処理 */
override def receive: Receive = {
case ImageGenerateMessage(id, url) => {
val downloadPath = downloadDir + "/" + id
val convertedPath = convertedDir + "/" + id
// 画像のダウンロード
imageStorage.download(url, downloadPath)
// 画像の変換
imageMagickService.convert(downloadPath, convertedPath)
// convertされた画像をバイナリで取得してDBに入れる
val bin = imageStorage.binary(convertedPath)
imageRepository.updateStatus(id, imageRepository.AVAILABLE, bin)
}
}
}
receive
で ImageGenerateMessage
を受け取ったら画像のダウンロードと加工をします。
Actorはメッセージを受け取ったら処理、受け取ったら処理、ということをひたすらするだけです。
HerokuのPostgreSQLには、容量の制限がないため、DBに直接画像のバイナリを入れることで、ストレージを用意する手間を省いています。 Heroku の PostgreSQL に容量制限ができてしまったため(僕のせいかもしれない)、現在はオブジェクトストレージを契約し、そこに画像をアップロードするように変更しました。
Controllerでは、Messageを投げた後レスポンスを返します。( https://github.com/yoshikyoto/lgtmoon/blob/master/app/controllers/ImageGenerateController.scala )
/** 画像生成を行うコントローラ */
class ImageGenerateController extends BaseControllerTrait {
/** 非同期で画像生成をするためのActor */
val imageActor = Akka.system.actorOf(Props(new ImageActor()))
/** postされたurlから画像生成をする */
def withUrl = Action.async { request =>
val jsonOpt = request.body.asJson
jsonOpt match {
case None => Future(NOT_JSON_RESPONSE)
case Some(json) => {
(json \ "url").asOpt[String] match {
case None => Future(PARAMETER_KEYWORD_NOT_FOUND_RESPONSE)
case Some(url) => {
// とりあえずURLだけ先に払い出して返す
ImageRepository.create() map {
case None => DATABASE_CONNECTION_ERROR_RESPONSE
case Some(id) => {
// 加工後の画像のURLになる予定のやつ
val lgtmUrl = UrlBuilder.imageUrl(id.toString)
// Actorに投げる
imageActor ! ImageGenerateMessage(id, url)
// レスポンスを返す
Ok(JsonBuilder.imageUrl(lgtmUrl))
}
}
}
}
}
}
}
}
Slick
Slickは非同期にDBIOを行うことのできるScalaのライブラリです。
http://slick.lightbend.com/
Slickの特徴は以下の点です。
- 非同期処理
- 関数型チックなクエリ生成
- コード自動生成
非同期処理
Scalaの特徴は非同期処理なので、DBアクセスのライブラリも非同期処理に対応していてほしいです。Slickは非同期処理に対応しており、結果はFutureで帰ってくるため、重いDBIOも効率的に処理できます。
関数型チックなクエリ生成
Slickは、List操作のような形でDBのクエリを書くことができます。
https://github.com/yoshikyoto/lgtmoon/blob/master/app/domain/image/ImageRepository.scala
例えば、Selectのクエリを見てみましょう。
/**
* 画像を最新20件取得する
*
* @param limit: Int 何件取得するか。デフォルト20
* @return Future[Option[Seq[ImageRow]]]
*/
def images(limit: Int = 20): Future[Option[Seq[ImageRow]]] = {
val action = Image.filter(_.status === AVAILABLE)
.sortBy(_.createdAt.desc)
.take(limit)
.result
db.run(action).map {
case images: Seq[ImageRow] => Some(images)
case _ => None
}.recover {
case e => None
}
}
トップページに表示する画像20件を取得するクエリです。sortBy.take と、関数型の特徴であるList操作に近い形でクエリを書くことができます。
コード自動生成
使い勝手にクセがあるのですが、Slickは、DBのテーブルからコードを自動生成する昨日があります。
今回はこのようなテーブルスキーマになっています。
create table Image (
id bigserial primary key,
content_type varchar(32) not null,
created_at timestamp not null default current_timestamp,
status smallint not null,
bin bytea default null
);
create index image_created_at on image (created_at);
これをDBに流した状態で以下のようなコードを書き
object Codegen extends {
def main(args: Array[String]): Unit = {
val slickDriver = "slick.driver.PostgresDriver"
val jdbcDriver = "org.postgresql.Driver"
val url = "jdbc:postgresql://localhost/lgtmoon"
val outputDir = "../app"
val pkg = "repositories"
val user = "postgres"
val password = "postgres"
slick.codegen.SourceCodeGenerator.main(
Array(slickDriver, jdbcDriver, url, outputDir, pkg, user, password)
)
}
}
このようにbuild.sbtを用意して sbt run
すると
このコードが自動生成されます。ここでは、Imageテーブルに対応するImageクラスと、テーブルの一つの要素に対応するImageRowクラスができます。これをもとに先ほどのようなクエリを書くことができます。
さいごに
今回はLGTMを簡単に生成するアプリケーションを作成する話でした。
今後つけたい機能
- 手元の画像をアップロードできるようにする
- 背景が白い画像や明るい画像、キャラクターなどの顔にLGTM画像がかぶってしまう時などへの対策
- 画像の検索やお気に入り的な機能