LoginSignup
55
29

More than 1 year has passed since last update.

LGTM画像を驚くほど簡単に作れるWebサービスをScalaで作る

Last updated at Posted at 2016-12-10

はじめに

Scala, Play Framework, Akka Actor, Slick を利用し、LGTM画像を驚くほど簡単につくれるWebサービスを作った話です。

LGTM とは

Looks Good To Me の略です。
GitHubのプルリクエストをレビューした時、「私は問題ないと思います」という意味で「LGTM」と書きこみます。

LGTMコメント例

LGTM画像とは

「LGTM」と4文字コメントするだけではそっけないので、Looks Good な画像を付けてコメントすることがあります。

LGTM画像の例

こういった画像を探すのに便利な http://lgtm.in/ というサービスがあります。(現在は閉鎖?されてしまったようです)

しかし、LGTM.in はあくまでも単なる画像共有サービスです。
LGTM.inに自分の気に入った画像がないので作りたい!もうワンランクの上のLGTM画像が欲しい!
でも画像を用意して文字を重ねて...とかするのは面倒だ!というのが僕の思いでした。

そこで、「なんかいい感じのLGTM画像を驚くほど簡単につくれる」Webサービスを作りました。

デモ

使い方

  1. 一番上の検索窓に、キーワードを入力します。
  2. 画像検索が走るので、LGTM画像のベースにしたい画像をクリックします。
  3. しばらく待つか、画面をリロードするとLGTM画像ができあがります。
  4. 出来たLGTM画像をクリックすると画像のURLやGitHubのMarkdownがコピーできるので、プルリクのコメントなどに貼り付けます。

より高度な使い方

  1. 一番上の検索窓に、LGTM画像のベースにしたい画像のURLを入力します。
  2. しばらく待つか、画面をリロードするとLGTM画像ができあがります。
  3. 以下同様です。

注意点

  • herokuの無料プランですので、動作が重かったり、容量がいっぱいになってLGTM画像が生成できなくなったりします。優しく扱ってください。 有料プランにしました。
  • どこかから怒られそうな画像は使わないようにしてください。
  • ソースコードは公開しています。

技術的な話

アプリケーション構成

アプリケーション構成は以下の通りです。
フロントでの画像表示はvue.jsを使っています。採用理由は、Reactよりシンプルに書けて、個人開発でのメンテナンスがしやすいからです。
画像生成に ImageMagick を、DBアクセスには Scala のライブラリである Slick を使っています。DBにはherokuのPostgreSQLを利用しています。

lgtmoon.png

画像生成フロー

フロー図は以下の通りです。ベースとなる画像のダウンロードと、LGTM画像の生成には時間がかかるので、Actorを使って非同期に処理しています。画像生成APIが叩かれると、Actorに画像生成メッセージを送り、即座にAPIレスポンスを返します。

image.png

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)
    }
  }
}

receiveImageGenerateMessage を受け取ったら画像のダウンロードと加工をします。
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画像がかぶってしまう時などへの対策
  • 画像の検索やお気に入り的な機能
55
29
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
55
29