のらぬこの日常を描く

ノージャンルのお役立ち情報やアニメとゲームの話、ソフトウェア開発に関する話などを中心としたブログです。

jetty + jndi(with c3p0)な環境から lightsleepを使って超簡単にDBに繋ぐ - デバッグができるようになるまで

どうも、のらぬこです。

この記事では、タイトルに記載の環境が eclipseデバッグ環境でも mvn jetty:run での環境でも正しく動作するところまでを記載します。

さて、最近、個人的に立ち上げてみたいWebサービスができまして、現在こっそり実装中です。

ただ、いかんせコード書き始めてまだ3日程度なので、使うフレームワークは今後変わるかもしれないし、そもそも途中で飽きるかもしれません。

が、今のところ、言語はJava、ApplicationServer には jetty、RDBpostgreSQL、Webフレームワークにはasta4dというフレームワークを選択しています。

O/R mapperとしてDomaを使おうかとも思ったのですが、最近見かけたlightsleepというO/R mapperを検証がてら使ってみることにしました。

下準備

開発環境

まずは開発環境として以下の環境を準備します。

  • Eclipse 4.5.2
    • すでにneon.2 まで出ていますが、そちらでも問題ないと思います。
  • run-jetty-run 1.3.5(Nightly)
    • Eclipse 内の market place からもダウンロードできますが、現在公開されているバージョンはjetty 9.0.0M3 までしか対応していません。
    • eclipseからデバッグ実行したとき、環境によっては(?)不具合が発生する可能性があるため、GitHub - xzer/run-jetty-run: Official successor of https://code.google.com/p/run-jetty-run/ を参考に、最新のnightly buildを使います。
      • market placeからダウンロードできる1.3.4を使用た場合、デバッガから起動した際に、使用するJettyのバージョンに関わらず、IndexOutOfRangeExceptionが大量発生する謎バグに遭遇することがあります。
    • なお、Eclipse market place を見ると、「Eclipse Jetty 3.9.0」 なるものもありますが、こちらのプラグインはjetty 起動時にc3p0を lib/ext としてjettyに食わせる手段がなさそうなので使えません。

とりあえずひな型WebAppの作成

これから開発を始めるという方は、まずはひな型Webプロジェクトを作成してください。

asta4dを利用する場合の手順は、GitHub - astamuse/asta4d: View first web application framework に手順が掛かれています。超簡単。

pomの設定を追加

下記のような形で書きます。とりあえず、今回追加が必要となる部分のみ抜き出しています。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <dependencies>
        <!-- https://mvnrepository.com/artifact/org.postgresql/postgresql -->
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <version>9.4.1212.jre7</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.mchange/c3p0 -->
        <dependency>
            <groupId>com.mchange</groupId>
            <artifactId>c3p0</artifactId>
            <version>0.9.5.2</version>
        </dependency>

        <dependency>
            <groupId>lightsleep</groupId>
            <artifactId>lightsleep</artifactId>
            <version>1.5.1</version>
            <scope>system</scope>
            <systemPath>${project.basedir}/lib/lightsleep-1.5.1.jar</systemPath>
        </dependency>
    </dependencies>
</project>

なお、lightsleepは mvnrepository に登録されてなさそうなので、こちらを参考に、jarをダウンロードして pom.xml に systempath として追加しています。

jndiの設定を書く

jetty側

プロジェクトディレクトリに jetty ディレクトリを作成し、その中に jetty-local.xml という名前で jndiの設定(jetty側)を記載します。 jndi名、jdbc接続文字列等は各自適切に変更してください。

<!-- jetty/jetty-local.xml -->
<Configure id="noranuk0DB" class="org.eclipse.jetty.server.Server">
  <New id="noranuk0MasterDB" class="org.eclipse.jetty.plus.jndi.Resource">
    <Arg></Arg> <!-- score -->
    <Arg>jdbc/noranuk0DB</Arg> <!--  name -->
    <Arg> <!-- value -->
      <New class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <Set name="driverClass">org.postgresql.Driver</Set>
        <Set name="jdbcUrl">jdbc:postgresql://localhost:5432/kagure</Set>
        <Set name="user">admin</Set>
        <Set name="password">admin</Set>
      </New>
    </Arg>
  </New>
</Configure>

webapp側

src/main/webapp/WEB-INF/web.xml の web-app要素内に以下を追記します。res-ref-name は jetty-local.xml で指定した名前と同じものを設定してください。

<resource-ref>
    <res-ref-name>jdbc/noranuk0DB</res-ref-name>
    <res-type>javax.sql.DataSource</res-type>
    <res-auth>Container</res-auth>
</resource-ref>

lightsleepの設定

src/main/resources/lightsleep.property を新規作成し、以下のような感じで設定します。Databaseの欄は環境に合わせて適宜変更してください。

# for PostgreSQL
Logger             = Std$Out$Info
Database           = PostgreSQL
ConnectionSupplier = Jndi
dataSource         = jdbc/noranuk0DB

各環境ごとの設定例などは、Lightsleep/Manual_ja.md at master · MasatoKokubo/Lightsleep · GitHub を参照してください。

run-jetty-runの設定

  • $HOME/.m2/repository/ 以下から下記3つの jarを探してきて、プロジェクトディレクトリ内の適当な場所(jetty/lib/ext 等)にコピーします。
    • c3p0-0.9.5.2.jar
    • mchange-commons-java-0.2.1.jar
    • postgresql-9.4.1212.jre7.jar
      • JDBCドライバは、接続先DBによって適切なものに読み替えてください
  • Eclipse から [Run] - [Debug configurations…] を選択します。
  • 左サイドペインから [Jetty WEbApp]を選択します。
  • Jettyタブを開きます。
    • Jettyのバージョンは 9.3系(もしくは一番新しいバージョン)を選択します。
      • 9.2未満のバージョンを選択した場合、謎バグを踏むという不幸が訪れる可能性があります。
    • 一番下にある、[Show advanced option] を選択します。
    • JNDI support のチェックを入れます
    • Additional Jetty.xml の欄に 上で作成した jetty-local.xml のパスを指定します。
  • Jetty classpath タブを開きます
    • リストを一番下までスクロールさせると[Custom Jetty Classpath]という項目があるのでそれを選択します。
    • 最初にコピーした3つの jar を追加します。

ここまでの作業をしたうえで、デバッグを開始すれば、おそらく正常に実行されるはずです。

DBアクセスのテスト

URLがたたかれた時に動作するコードに、こんな感じのコード埋め込んであげると確認できるかと思います。

Transaction.execute(connection -> {
            Optional<Item> contactOpt = new Sql<>(Item.class)
                .where("id={}", 1)
                .select(connection);
            // 取得したレコード出力するなりなんなり
});

mvn jetty:run 向け pom.xml の設定

そろそろ書くのに疲れてきたので、jetty-maven-pluginの設定部分のみ貼っておきます。

こっち側は、jetty + jndi(with c3p0) が正しく動作するように設定を記載するだけです。

dependencies に c3p0 と jdbc drive を追加してあげないと、jettyが起動時にjndiの設定を行う際、c3p0などが認識できずエラーになってしまうので、ちゃんと書かないとだめです。

<project>
    <build>
        <plugins>
            <plugin>
                <groupId>org.eclipse.jetty</groupId>
                <artifactId>jetty-maven-plugin</artifactId>
                <version>9.4.0.v20161208</version>
                <configuration>
                    <webApp>
                        <contextPath>/</contextPath>
                    </webApp>
                    <httpConnector>
                        <port>8080</port>
                    </httpConnector>
                    <daemon>true</daemon>
                    <jettyXml>${project.basedir}/jetty/jetty-local.xml</jettyXml>
                </configuration>
                <executions>
                    <execution>
                        <id>start-jetty</id>
                        <phase>pre-integration-test</phase>
                        <goals>
                            <goal>start</goal>
                        </goals>
                        <configuration>
                            <scanIntervalSeconds>0</scanIntervalSeconds>
                        </configuration>
                    </execution>
                    <execution>
                        <id>stop-jetty</id>
                        <phase>post-integration-test</phase>
                        <goals>
                            <goal>stop</goal>
                        </goals>
                    </execution>
                </executions>

                <dependencies>
                    <dependency>
                        <groupId>org.eclipse.jetty</groupId>
                        <artifactId>jetty-io</artifactId>
                        <version>9.4.0.v20161208</version>
                    </dependency>

                    <!-- https://mvnrepository.com/artifact/org.postgresql/postgresql -->
                    <dependency>
                        <groupId>org.postgresql</groupId>
                        <artifactId>postgresql</artifactId>
                        <version>9.4.1212.jre7</version>
                    </dependency>

                    <!-- https://mvnrepository.com/artifact/com.mchange/c3p0 -->
                    <dependency>
                        <groupId>com.mchange</groupId>
                        <artifactId>c3p0</artifactId>
                        <version>0.9.5.2</version>
                    </dependency>
                </dependencies>
            </plugin>

            <!-- -->
        </plugins>
    </build>
</project>

今回の話は以上となります。

lightsleep の使用感など、何かネタが出てくればそちらも記事にしたいと思います。

お読みいただいてありがとうございました。

gulp-s3-gzipで、s3上の格納先のパスを指定する方法

こんばんは、のらぬこです。

webサーバからクライアントにコンテンツ圧縮してを送ることができる仕様があります。

今更感半端ないですが、リクエストヘッダに書く accept-encoding : gzip とか、レスポンスヘッダにcontent-encoding : gzip と書いて圧縮したコンテンツを返すとかの話です。

少しでも転送量を減らしたいとき、少しでもページ読み込み速度を上げたい時などに使われるものだと思います。

ところで、s3に格納されたファイルについては、webブラウザが圧縮コンテンツ送ってもいいって要求してきても(つまり、accept-encoding : gzip と書かれていたとしても)、s3はわざわざ圧縮したコンテンツを返してくれません。

その辺は自前でやってくれってことですね。

取り敢えずやってみるだけであれば、以下の記事などが参考になると思います。

d.hatena.ne.jp

こちらの記事を書いた方も言われていますが、毎回毎回こんなことやってたら精神的に死んじゃうので、自動化したいですよね。

例えば、gulp を使う場合、gulp-s3-gzip というモジュールを使うと、ファイルを自動でgzip圧縮したうえで、自動でs3にアップロードできます。

しかも、content-encoding のヘッダ指定などもやってくれるので、deployはコマンド一発で完了です。

www.npmjs.com

npmjs内のドキュメントを読むと、ちょっと困ったことにアップロード先のパス指定のやり方が記載されていません。

やり方は簡単で、npm install して引っ張ってきたソース見るとすぐわかります。

var gulp = require("gulp");
var s3 = require("gulp-s3-gzip");
var gzip = require("gulp-gzip");
var options = { gzippedOnly: true, uploadPath : "/hoge/fuga/" };
 
gulp.src('./dist/**')
.pipe(gzip())
.pipe(s3(aws, options));
 
});

こんな感じで、options に uploadPath : "/hoge/fuga/" を追加すればおっけーです。

なお、gulp-s3-gzipの詳しい説明や、ソースコード全体を閲覧したい場合には、上でも紹介した gulp-s3-gzip のページを参照してください。

ということで、今回のお話は以上となります。

どなたかのお役に立てれば幸いです。

【2016年後半版】個人公開アプリのダウンロード数、収益を少しでも増やすためにやったこととその成果

はじめに

こんにちは。

現在、android 向けに4本のアプリを公開しています(ゲームではなくアプリです)。 そのうち、2014年12月末に公開したアプリが、比較的順調にダウンロード数を伸ばし、先日35万ダウンロードを突破しました。

ちなみに、2017年の1月12日現在の総ダウンロード数は37万5374となっています。

なお、このテーマで記事を書くのは今回で3回目になります。

一応、過去記事のリンクも掲載しておきます。

もしお時間ありましたら、最初の記事からお読みになられると、改善していくための過程などが順に追えるのでより理解しやすいかもしれません。

qiita.com

noranuk0.hatenablog.com

35万ダウンロードされたアプリの紹介

記事内でアプリの紹介をしたことがなかったので、せっかくなので簡単に紹介します。

media.Re.Scan:高速メディアスキャンアプリ

このアプリは、いわゆる「メディアスキャン」という呼ばれるアプリです。

アンドロイド端末に保存された音楽ファイル、動画ファイルは、ファイルシステム上のパス情報とは別に、メタ情報(例えば、アルバムタイトル、作曲者、曲名、動画のサムネイル画像などです)が端末内のデータベースに保存されています。

ちなみに、このデータベースや、その情報を管理している内蔵アプリのことを「メディアストレージ」とよんだりしています。

さて、ファイルシステム上のファイルパス情報と、メディアストレージの情報は、通常はアプリやアンドロイドOSの機能により適切に同期されているのですが、行儀の悪いアプリの影響などで同期が正しくとれなくなってしまうことがあります。

こうなってしまうと、音楽再生アプリや、動画再生アプリなどで、メディアファイルの情報が正しく取得できなくなり、アルバム検索ができないなどの不具合の原因になる場合があります。

これを正しい状態に修正するのが、今回の記事テーマにもなっている、拙作「media.Re.Scan:」というアプリです。

なぜ作ったか

実は、自分が開発を始めた時には、すでに類似のアプリがたくさんありました。

しかし、Android4.4の仕様変更により、既存のアプリはほとんど動作しなくなってしまったこと、Android4.4に対応したメディアスキャンアプリでまともに動作するものが、当時はまだリリースされていませんでした。

自分も必要だったことと、今出せばそこそこのヒットが狙えるんじゃないかと考え、自分で作ってみることにしたのが始まりです。

そんなこんなで

そんなこんなで、記事タイトルにもあるように、ダウンロード数、収益を少しでも増やすために行った各種施策が功を奏し、昨年7月頃から、1日平均1200〜1500ダウンロード、昨年12月の末ごろからは1日1750ダウンロード以上を維持し続けています。

施策と成果の話

というわけで、早速ですが、行ってきた施策とその振り返りの話をします。

ネイティブ広告の話

昨年夏に導入したネイティブ広告ですが(詳細は前記事参照です)、まずはその経過報告です。

ネイティブ広告は、スキャン中の待ち時間に、画面の半分くらいを被うサイズのものを表示させています。

ただ、常に表示させているわけではなく、スキャン時間が長くなりそうな時に一定の確率で表示させています。

同一の期間で、バナー広告とネイティブ広告で、表示回数、収益を比較してみると、

ネイティブ広告
表示回数:10万回 収益:1万1千円

バナー広告(全体)
表示回数:200万回 収益:13万円

程となっています。

数字だけ見ると、ネイティブ広告の方が(表示回数あたりの収益額という見方をすれば)高いのですが、波が大きく、とくにここ最近はあまり収益に貢献してくれていません。 単純に表示確率をあげれば収入が増えるとか、逆に下げたほうが増えるということにはならないと考えています。

とりあえず、両方同時に出すことも画面レイアウト上は可能な作りにしているのですが、規約上、1つの画面に複数の広告を表示できないことになっているので、ネイティブ広告とバナー広告を両方同時に出すことはできません。

CTRの高い方を選択できるような仕組みがあればいいのですが、現状はそういった仕組みはなさそうなので、そこは残念なところです。

アプリ開発者の収益を最大化させるために、その辺の仕組みも用意してくれるか、ネイティブ広告とバナー広告の同時表示がおっけーって規約に明示していただけると嬉しいです。→Google殿

インタースティシャル広告の導入

最近、グーグルから送られてくる「admobがインタースティシャル広告対応したから使ってよ」っていう宣伝メールがうるさいので、試験的に導入してみました。

ネイティブ広告は、画面は占有するけど、ユーザのアクションは必要ない広告です。
一方で、インタースティシャル広告は、クローズボタンで広告を閉じるか、広告内のコンテンツをタップして画面遷移するまで広告が消えない=アプリの操作ができないというタイプの広告のため、ユーザ的にもストレスになりやすいです(少なくとも僕は)。

試験的ということで、現在は、比較的長期間使い続けてくれているユーザさんに対して、ネイティブ広告を表示する条件にマッチした場合、10%くらいの確率でインタースティシャル広告を表示しています。

基本的には、メディアスキャンの待ち時間に表示されていて、広告表示中もバックグラウンドで処理は続行中のため、ユーザさんにとって、広告を閉じるという手間は増えてしまいますが、時間的な不利益はほぼないはずです。

ゲームの場合は、ステージクリア後などに一定の確率で表示させることで、比較的収益に貢献してくれるらしいのですが、アプリの場合にはといいますと、

広告表示比率を絞りすぎた結果、ほとんど表示されておらず、集計可能なデータ量が集まっていない事案発生

広告に対して、ネガティブなイメージを持たれる方が多いのは、日本も海外も一緒のようで、広告嫌い的なレビューがつくことも何度かありました。

そういったレビューがつかないように、表示頻度がかなり少なくなるように設定したのですが、ちょっと少なくしすぎたようです。

約一ヶ月様子を見てみましたが
表示回数:131回 クリック数:2回 収益額:12円

という結果です。

クリック率は高いのですが、データが少なすぎてなんとも言えません。

ただ、1クリックあたり6円程度ならバナー広告と変わらないため、よく言われている「インタースティシャル広告はクリック単価が高い」ということは、少なくともGoogleのインタースティシャル広告に限って言えば、今の所まったくなさそうです。

レビュー訴求

★×5評価ってやっぱり大事です。平均評価4以上が超大事な文化なので、4でもちょっと・・・と思ってしまいます。

個人的には、平均の3以上ならまあいいじゃないのって思うんですけど。。

レビュー訴求はこれまであまりやってこなかったのですが、少しアイデアが出てきたのでやってみることにしました。

どのようなアイデアかというと、スキャン中、ネイティブ広告やインタースティシャル広告を表示する条件にマッチしなかった時に、レビューのお願いを表示してみるというものです。

たいていのアプリは、レビューのお願いをモーダルダイアログで表示するのですが、media. Re.scan: の場合、Activityの上にFrameLayoutを重ね、その上側のレイヤーにコントロールを配置しているだけです。

したがって、レビューのお願いが表示されていても、スキャンの中断などの操作は行うことができるようになっています。

f:id:noranuk0:20170113130845p:plain

結果、1日あたりのレビュー数は増えたような気はしています。

developer console画面で、レビューの平均評価は確認できるのですが、レビュー数のグラフは標準では用意されていないため、感覚値でしかないのですが、レビューが追加されましたというプッシュ通知が来る頻度は、だいたい倍ぐらいに増えたと思っています。

収益について

2016年の収益は?

やっぱり気になる人も多いかと思いますので、さらっと公表してみます。

というか、3年目で、やっと公表できるくらいの数字は出てくるようになりました。

f:id:noranuk0:20170113130334p:plain

結果的には、16万5千円ほどでした。 収益が伸びてきたのは、6月頃からで、今の所月額2万〜2万5千円ほどで頭打ちになっています。
今年もこのアプリだけで収益を増やしていくことができそうか、というと、さすがにそういうことはなさそうです。

とはいえ、10万DLクラスのアプリアイデアも、そうそう出てくるものではないのも悩みどころです。

今年中には月額5万、数年以内に10万くらい稼げたらいいなーっという感じではあるので、何かやってみたいところではあります。

個人的にお勧めの書籍紹介

最後に、おすすめ書籍の紹介です。

個人でアプリ作って儲けたいなら全力でお勧めしたい本。 ゲームのマネタイズについての話がメインですが、ツール系アプリを開発されている方にも参考になりそうな情報も色々と載っています。

また、いろいろな本を読み漁ってみたいという方は、kindle unlimited に加入するのも割とお勧めです。

kindle unlimited は、月額980円(2017年7月現在)のみで、kindle unlimitedの対象書籍を無制限に読むことができるサービスです。

対象書籍のラインナップも、上で紹介した書籍も含め、個人で出版された書籍や市販の技術書等、結構豊富に取り揃えられています。
月額980円ですが初月は無料なので、お試しでひと月だけ加入して、ずっと持っておきたい本だけ購入するみたいな使い方もありかと思います。

kindle white paper 等の kindle端末を持っていなくても、androidiOS端末をお持ちであれば、アプリ版のkindleから利用することもできます。

もし興味ございましたら、kindle unlimited の対象書籍を幾つかリストアップしておきますので、ご参考までにどうぞってことで。

kindle unlimited の申込みは下記リンクから行えます

Kindle Unlimited会員登録

といったところで、今回のお話は以上となります。

皆様にとって何かの参考になれば幸いです。

どうもありがとうございました。

前の記事

noranuk0.hatenablog.com

全部無料⇒VSTSとAzureを連携させて、nodejs+expressなアプリの自動デプロイ環境を実現する

こんにちは。のらぬこです。

今年も残り1か月、コミケ以外はもうどうでもいい感じの季節になってきました。

今日は、VisualStudio TeamSerivices内のgitリポジトリに格納されたnodejs+express製のWebアプリを、(今のところは)永久無料でAzureにホスティングしてもらう話をします。

題材として、前回の記事で作成した「nodejs+express製のあぷろだアプリ」をAzureに載せてみたいと思います。

noranuk0.hatenablog.com

gitリポジトリへの登録

まずはプロジェクトをVisualStudio TeamSericesのgitリポジトリにコミットします。 こちらの記事を参考に、VisualStudio TeamSerivcesのアカウントを作り、uploaderプロジェクト全体を、gitリポジトリにpushしてください。

noranuk0.hatenablog.com

なお、node_modulesと、uploadsディレクトリは .gitignoreに追加するなどしてcommit対象から除外してください。

f:id:noranuk0:20161205234631p:plain

Azureアカウントを作成する

すでにアカウントをお持ちの方は Microsoft Azure からダッシュボードに入ってください。

f:id:noranuk0:20161206001913p:plain

アカウントをまだお持ちでない方は、ここからアカウントを作成してください。登録時にクレジットカード要求されたりSMS認証があったりしますが、お金がかかるサービスは一切使いません。

なお、新規登録時に200ドルほどの無料クレジットがもらえますが、こちらも利用しません。

ちなみに新規登録時にもらえる無料クレジットなのですが、一か月ほどで使用期限が切れてしまいます。こちらは各種実験など何か別の用途で使い切ってしまってもよいかと思います。

アプリのデプロイ先を準備する

まずはWebアプリをデプロイするためのインスタンスを作成します。

新規(+ボタン) > Web+モバイ > Web App の順に選択します。 f:id:noranuk0:20161206002859p:plain

表示画面が切り替わるので、まずは、アプリ名、リソースグループ、サブスクリプションを設定します。

デプロイしたアプリには、http://{アプリ名}.azurewebsites.net でアクセスできます。

hoge, huga, wp, myapp等の適当な名前はすでに誰かに使われていて、かつ試しに繋いでみると403だったりするので、自分のわかり易い名前をちゃんとつけたほうが良いかと思います。

次に、料金プランを無料プランに変更します。

App Serviceプラン場所 > 新規作成 > 価格レベル(S1 Standard) f:id:noranuk0:20161206002938p:plain

おすすめプラン一覧が出てくるのですが、この中には無料で使えるプランは含まれていません。

画面右上の「全て表示」を押して使用可能な全てのプランを表示してください。

f:id:noranuk0:20161206003502p:plain

一番下までスクロールさせると、「F1 free」というプランがあるのでそれを選択してください。

f:id:noranuk0:20161206221204p:plain

なお、このプラン、お金はかからないようにはなっていますが、いくつか制限が存在します。

  • 独自ドメインは使えません
  • SSLは使えません
  • CPUやメモリー、ディスクスペース等のスペックはは記載の通りです
  • 自動・手動スケールアウトは出来ません
  • 一定時間あたりのCPU占有率、データ転送量の上限が決まっています
    • 上限を超えた場合、一定時間サービスが停止されます
    • お金を払って制限解除、みたいなことは出来ないようです
  • 他にもあるかもしれません

外部公開してアクセスを稼ぐような用途にはまず向きませんが、個人用途でそんなに負荷もかからないサービスとして利用するのであれば、さほど困ることは無い程度のスペックはあると思います。

AzureをVisualStudio TeamSerivcesと関連付ける

Visual Studio Team SerivicesにコミットされたプロジェクトをAzureにデプロイするには、事前に2つのサービスを関連付けておく必要があります。

ダッシュボードのメニューから、Team services accounts を選択します。

f:id:noranuk0:20161206221619p:plain

ログイン中MicrosoftAccountに紐付いているMSDNアカウントや、VSTSアカウントが表示されるので、紐付けたいアカウントを選択してください。

参考サイト)Setting up a VSTS account so it can deploy to a Web App · projectkudu/kudu Wiki · GitHub

デプロイ元ソースにVSTSのgitリポジトリを設定する

Web App」を選択し、先程作成したアプリインスタンスを選びます。

更に、「アプリのデプロイ」カテゴリー内の「デプロイオプション」を選択します。

f:id:noranuk0:20161206222042p:plain

展開元」の「ソースの選択」を選ぶと選択可能なソース一覧が表示されます。

今回は、「Visual Studio Team Services」を選びます。

プロジェクトとブランチを選んで、OKボタンを押せば、最新コミットが自動でWebサーバに展開されます。

ブラウザから、「http://{アプリ名}.azurewebsites.net」にアクセスすると、VSTSにコミットされているアプリが動作しているのが確認できるかと思います。

また、VSTSリポジトリを更新すると、都度デプロイが走り、公開サれているWebアプリも更新されます。

最後に

ちなみに、ASP.NET等の MS謹製環境だけではなく、JAVAプロジェクトや、PHPなんかも動かすことが出来ます。

更に、ローカル環境のみですが、MySQLも使えるため、おそらくWord Pressも動かすことができるような気がしています。

Javaが動くので、scalaやgroovyやJRubyといったJVMで動く系言語(とその実装)も動かすことができます。

個人開発のタスク管理用に、Redmineクラウドに立てておきたい場合などにも活用できるかもしれません。

今回は、Visual Studio Team Serivcies と Azure を使えば、開発から公開まで完全無料のWebアプリ開発、ホスティング環境が手に入りますよ、というお話でした。

お読みいただいてありがとうございました。

nodejs+expressでオレオレあぷろだを作る ~ 其の二

こんにちは。のらぬこです。

1週間ほど前にこんな記事を書きました。

noranuk0.hatenablog.com

ゲーム機等で撮ったスクショ画像を自PCに転送するために、オレオレあぷろだを自作して、ゲーム機のWebブラウザから自PCにファイルをアップロードする方法を書いた記事です。

前回の記事で、nodejsのexpressにmulterという拡張モジュールを追加してこんな感じのものを作りました。

f:id:noranuk0:20161118013335p:plain

まあ、一応動いてはいるんですが、あぷろだを名乗ってるのにファイルリストも見られないのはちょっといただけません。

今回は、前回作ったあぷろだもどきを拡張して、アップロード済のファイルリストを見られるようにしてみます。

前準備

前回の記事を参考に、冒頭に載せたページが表示できるところまで実装を進めてください。

今回作ったもの

今回作ったのは、下の画面のようなものになります。

f:id:noranuk0:20161126184516p:plain

前回の作った画面と比べると、登録されているファイル一覧が追加されています。

とりあえずソース

とりあえず、前回記事で作った環境にこのソースコピペして上書きしたら動くよっての貼っときます。

// package.json
{
  "name": "uploader",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "start": "node ./bin/www"
  },
  "dependencies": {
    "body-parser": "~1.15.2",
    "cookie-parser": "~1.4.3",
    "debug": "~2.2.0",
    "express": "~4.14.0",
    "jade": "~1.11.0",
    "morgan": "~1.7.0",
    "serve-favicon": "~2.3.0",
    "multer": "~1.2.0",
    "date-utils": "1.2.21"
  }
}
// ./app.js

var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var dateUtils = require('date-utils');
var fs = require('fs');

var multer = require('multer');

var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');

// uncomment after placing your favicon in /public
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));


app.get('/', function(req, res){
    fs.readdir('./uploads', function(error, list) {
        if (error) {
            // TODO:
        } else {
            res.render('index', {'files' : list});
        }
    });
});

var upload = multer({ storage:
    multer.diskStorage({
      destination: function (req, file, cb) {
            cb(null, './uploads');
        },
      filename: function (req, file, cb) {
          var dt = new Date();
          var formatted = dt.toFormat("YYYYMMDD_HH24MISS");
          cb(null, '[' + formatted + ']' + file.originalname);
        }
    })
});
app.post('/', upload.single('uploadedfile'), function (req, res) {
    console.log(req.file);
  res.redirect(301, '/');
});

app.get('/files/:file', function(req, res){
    fs.readFile('./uploads/' + req.params.file,
        function(err, data) {
        res.send(data, 200);
    });
});


// catch 404 and forward to error handler
app.use(function(req, res, next) {
  var err = new Error('Not Found');
  err.status = 404;
  next(err);
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;
// ./views/index.jade

extends layout

block content
  h1 ファイルの登録
  form(method="post", enctype="multipart/form-data", action="/")
    input(type="file", name="uploadedfile")
    input(type="submit")

  h1 登録されているファイル
  ul
    for val in files
      li
        a(href="files/#{val}") #{val}

動かし方は、前回同様

npm install
npm start

です。
サーバが起動したら、ブラウザなどから http://{ip address}3000/ に繋いでください。

補足説明とか

前回からの変更点中心に補足説明です。

前回からの変更点1

保存するファイル名に入れていた日付をYYYYMMDD_HH24MISS の形式にしました。

          var dt = new Date();
          var formatted = dt.toFormat("YYYYMMDD_HH24MISS");
          cb(null, '[' + formatted + ']' + file.originalname);

このために、date-utils というパッケージを新たに追加しています。

前回からの変更点2

保存済のファイル一覧を表示するようにしました。

app.get('/', function(req, res){
    fs.readdir('./uploads', function(error, list) {
        if (error) {
            // TODO:
        } else {
            res.render('index', {'files' : list});
        }
    });
});

ディレクトリ内のファイルを列挙するのに fs というモジュール使ってます。nodejs 組み込みなので、package.json 書いて npm install しなくても使えます。

fs には、同期版(処理が終わるまでメソッドから戻らない)と、非同期版(処理が終わるのを待たずにメソッドから戻る。処理が終わったらコールバックが呼ばれる)の2つがありますが、今回は非同期版を使っています。

fileの一覧を、jadeに files という名前で渡しています。

jade側で、これを変数のように使うことができます

    for val in files
      li
        a(href="files/#{val}") #{val}

前回からの変更点3

保存済みファイルをのリンククリックでファイルをダウンロードできるようにしました。
自分のPCに上がっているファイルをわざわざブラウザから見る機会なんてそうそうないとは思いますが。

app.get('/files/:file', function(req, res){
    fs.readFile('./uploads/' + req.params.file,
        function(err, data) {
        res.send(data, 200);
    });
});

リクエストルールを記載する部分に :file のように書けば、リクエストURLの対象部分が、 req.params.file のような形で取り出すことができます。

また、正規表現で縛ることもできて、その場合には :file([A-Z0-9]{8}\.[A-Z0-9]{3}) のように書きます。

メソッドの中身は、対象のファイルを全部読み込んで、読み込んだデータをそのままレスポンスとして返しているだけです。

基本用途が画像のみなので取り敢えずこんな感じの実装にしましたが、例えば、ファイルサイズが10Gbと借る場合、それを一旦全部メモリーに読み込むことになるので、用途によってはもうちょっとどうにかしたほうが良い(というかすべき)と思います。

詳細な解説、説明などは、公式ドキュメントやQiitaなどの記事を参考にされるとよいかと思います。

今回の記事は以上となります。

お読みいただいてありがとうございました。

【twitter】不快系のプロモ広告をTLから抹消する方法

こんにちは、のらぬこです。

今回は、twitterのプロモーション広告についてです。

twitterのプロモーション広告はtwitter社の収益源であり、これをタイムラインから削除することはできません。しかし、特定の広告をTLから消すことは可能です。

今回はそんな話をします。

twitterをwebや公式アプリから見ていると、TL内に「プロモーション」とタグ付けされたツイートが混ざっていると思います。プロモツイートとか呼ばれていて、要はツイッター版ネイティブ広告のようなものですね。

そこそこの数が紛れ込んでくるのでちょっとウザいこともありますが、twitter社の収入源でもありますし、収益を得なければサービスの継続もできません。

したがって、webや公式アプリを使っている限りはこれを消すことはできないようになっています。

稀におもしろ系ネタ広告も流れてくるし、ツイートの中に広告を混ぜるというやり方はよいアイデアと思います。

ただ、最近は減ってきた気もしますが、ちょっとアウトって言うレベルの出会い系広告や、怪しい商材系の広告、最近ですと、「街角法律相談所」というアカウント名で「国が認めた借金の減額方法」というタイトルのプロモ広告が頻繁に流れてきたり、いくら寛容な僕でもこれは酷いって感じたり、不愉快になる系の広告が紛れ込んでくることも多々あります。特に iPhone端末では比較的多いような気がします。

f:id:noranuk0:20161122174901j:plain:w320

こういう広告に限っていえば、掲載前の審査で消すなり除外してくれるなりしてくれてもよいと思うのですが、現状はそうなっていません。

一応違反報告はすることができるようになっています*1

ただ、違反広告をすると、この広告がなぜ違反しているのかを選択する画面が表示されるのですが、上のような広告を報告するのに都合のいい選択肢は見当たりません。そもそも、ちょっとオトナ向け出会い系、真偽不明の情報商材くらいであれば規約違反にはなっていないのかもしれないですし。

f:id:noranuk0:20161122222516p:plain:w320

さらに、報告しても対処してくれるのかはわからないし、しばらく使ってると同じ広告が何食わぬ顔で再び出てくることもあります。

また、広告の色使いが派手で目がチカチカする、広告写真に写ってる写真の顔がムカつく、など、違反はしていないものに関しては、通報してどうにかなるものでもありません。

この手の迷惑広告は「気にせずスルー」でもよいのですが、個人的には、とりあえずタップしてtwitter社への成果報酬を発生させたうえで、ユーザをブロックしてしまうのが手っ取り早くておすすめです。ブロック中ユーザのプロモ広告は配信されないようになっているっぽいです。

なお、ツイートを非表示にする「ミュート」というのもありますが、ミュートしてもプロも広告だけはしれっと配信されてくるため、こちらでは効果は無いようです。

まとめ

ということで、今回は

ウザい系、不快系プロモーション広告打ってくるユーザは、通報して対処されるのを待つくらいならサクッとブロックしちゃったほうが楽ですよー

というお話でした。

余談とか

余談になりますが、これら不快系プロモ広告、見かけたらとりあえずクリックして広告主のWebサイト(アプリダウンロードを促す宣伝ページ)に遷移してしまうのも有効かもしれません。

プロモ広告を掲載するにはお金がかかります。

広告主(今回の場合は迷惑広告を配信しているアカウント)は、予算枠を決めてプロモ広告広告を出します*2

プロモ広告がクリックされてページ遷移すると予算が消費されます。最初に決めた予算枠を使い切ると、広告主がtwitter社に追加で入金しない限りはプロモ広告の配信が停止されます。

つまり、とりあえずリンクをクリックして、予算枠を枯渇させてしまえば、それ以降広告が配信されることもなくなるわけです。

なお、一人で何回もクリックしても、おそらく重複クリックとみなされて、予算消費がキャンセルされます。

見かけたら取り敢えず1回だけクリック&遷移をみんなで心がければ、予算枠が即消費され、TLからは存在が抹消されると思わ れます。

ただし、宣伝用のWebページに遷移した後は何のアクションも起こさず即バックボタンで戻るようにします。

商材やアプリの開発元とTwitter広告を出した人が別事業者の場合、Twitter広告主は宣伝用ページ経由で起こされたアクション(アプリダウンロードや商材申し込みページへの遷移、アンケートの回答など)によって開発元から収益を得る仕組みになっていることが多いです。

また、同一業者の場合にも、広告にかけた費用に対して売り上げ(アプリダウンロードや商材の購入)があまりに低いと判断すれば、広告を打つのをやめてくれるでしょう。

つまり、そのページで何らかのアクションを起こしてしまうと、せっかくの行為が迷惑広告主の収益になってしまう可能性があります。

というわけで今回は、不快系プロモ広告を抹消する方法を書かせていただきました。

この記事が何方かのお役に立ちますと幸いです。 お読みいただいてありがとうございました

*1:Android版公式アプリの場合には、プロモーションツイート右上の『く』を上向きにしたようなマークをタップするとメニューが出てきます

*2:アカウントを作ると初回いくらか分は無料っていう枠をもらえるので、その範囲でしか出していないのかもしれませんが

やはりQiitaのいいね仕様はまちがっている。

こんにちは。のらぬこです。

Qiitaという、ソフトウェア開発エンジニア向けのサービスがあります。

「ソフトウェアエンジニアをやっている(目指している)方向けに特化した、技術ブログサービス的なもの」と僕は認識しています。

qiita.com

さて、そのQiitaさんなのですが、最近、機能追加とそれに合わせたデザイン変更が行われました。

今まで、「記事のストック」という機能が「記事のブックマーク」+「記事へのフィードバック(よかったよ!という意思表示)」の役割を果たしていました。

今回、新たに「いいね」を追加し、「ストック」はブックマーク目的、「いいね」は記事へのフィードバック目的という風に、役割を分割しました。

f:id:noranuk0:20161120175632p:plain

その辺の経緯と、機能の説明、使い方などは、Qiitaさんの公式アナウンスに書かれています。

投稿記事やコメントに「いいね」できるようになりました(Contributionの算定基準も変わります)... - Qiita Blog

この変更意図は分かります。

しかし、僕は今回のQiitaの変更は、「記事の価値を表す指標とブックマークとしての指標を明確に分離し、記事の価値を定量的に図る指針を作るための機能を追加したい」という運営会社の都合を、エンドユーザの使い方を制限するというやり方で押し付けただけの、とてもナンセンスな変更だと感じています。

その上で、なぜそう感じたのかじゃあどんなものならいいと思うのか、を書いていこうと思います。

なぜナンセンスと感じたのか

根底にあるのは、「いいね」と「ストック」、どちらも「記事とユーザを何らかの目的で紐づけている情報」という意味では同じもののはずなのに、機能的な制約を課すというやり方で、2つを使い分けることを強要しているということだと考えています。

僕が感じた機能的制約とは

  • 記事のストック数が記事上には表示されなくなった
    • 代わりに「いいね」の数が表示されるようになった。
  • 自分がストックした記事の一覧は後から見ることができるが、いいねした記事の一覧はいいねした本人含め誰も閲覧できない
    • 記事を開けば、いいねしてくれたのが誰なのかは(以前のストックと同じく)見ることができる。
  • 「いいね」ボタンは目立たせているのに、「ストック」ボタンは薄めの色を使いワザと(?)目立たないようにしている?

の3つです。

これらの3点について、なぜそうしたのか、をなんとなく推測してみました。

まず、記事のストック数が表示されなくなった理由についてですが、今までは「良記事」の指標として「ストック数」を出していたけど、その役割は「いいね」が担うことになり、「ストック数」は表示する必要性がなくなった。

いいねした記事の一覧が見られない理由は、それを出してしまうと、「ストック」と「いいね」の機能的な差異がなくなってしまい、「いいね」が「これまでのストック」と同じ使われ方をされることを危惧したのではないかと考えます。
まあ、まず間違いないと思っています。
どう使うかはユーザが考えるものだと思うので、もしそうだとしても、それを危惧する必要は全くないと思うののですが。

ストックボタンをかなり控えめなデザインにした理由は、正直よくわからないです。リリースノートには、今後、ストックは一時的に使うものという記載がありますが、あそこまで目立たなくさせる必要性は感じられないし、そもそも「いいね」は「ストック」の代替にはなっていないし、置き換えられるものでもないでしょう。

じゃあどんなものならいいと思うのか

唐突ですが、ニコニコ動画のマイリストの仕組みはとても優秀だと思っています。

一般会員とプレミアム会員で数の制限とかは違っていたと思いますが、「とりあえずマイリスト」が、とりあえず保存した動画、これ以外に自由な名前のマイリストを何個か作れて、自分の気に入った動画を好きなように分類できるようになっています。

機能仕様としては、あんな感じでよかったんじゃないかなーと個人的には思います。

「とりあえずストック」と「ストックに追加」を用意して、「ストックに追加」を選んだ場合には目的別にカテゴライズできるようにするイメージです。

ストックする理由は人それぞれだし、Qiita側が「同じデータ」を「それぞれにこういうインターフェースを用意したので、このように使ってください」とレールを用意してあげる必然性は全くないはずです。

「とりあえずストック」に今まで通りため込んでもいいし、「後で読む」「良記事」「俺の教訓」みたいにカテゴリー分けして保存してもいいし。

記事のコントリビューションについても、以下のような感じで

  • とりあえずストック = 0.8
  • その他の名前付きストック = 1.0
  • 一人のユーザが、同じ記事を複数のストックに入れた場合もスコア上限は1

という感じで、なんとなくしっくりくるんじゃないかと思います。

少しだけ余談

僕は、記事の「ストック」機能を、あとで読もうと思った記事、教訓的な記事など何度か読み返したい記事などを記事を保存(ブックマーク)する目的で活用してきました。

Qiitaさんもご推察の通り、はてなスターのように面白かった記事や参考になった記事へのフィードバックとしても使っています。

ストックした記事の一覧を見返してみると、あとで必要になりそうだと感じたからストックした系の記事が2割、はてなスターのような意味合いで使ったのが8割位です。

ただし、少し時間が空いたとき等、なんとなくストック記事一覧を眺めることはありますが、後から読み返そうと思ってストックした記事をストック記事一覧から探して読み直しにいったことはあまりないと思います。

これは別にストック記事一覧が使いずらいというわけではなく*1、アドレスバー検索ワードを入力してリンクの色が変わっている(過去に開いたことのあるページ)ページを再度開くほうが、ストック記事一覧を漁るよりも知りたい情報にたどり着くまでの負担が少ないからです(結果、寄り道してしまうこともありますが)。

とりあえずまとめ

しばらくは、Qiitaさんの推奨する使い方をしてみようと思います。

じゃあお前はストック一覧を活用していたかと言われれば、そうでもないかなーという気もするし、それなら、いいねした記事一覧がなくなったとしても実はあまり困らないかもしれないし。

この記事を書く際に参考にさせていただいた記事

今回の記事は以上となります。

お読みいただいてありがとうございました。

*1:ストック記事の記事タグでの絞り込みくらいは実装してほしいなーとは思いますが