Androidのメモとか

ポキオの日記です。今日も遅延してない。

GooglePhotosの写真をMarkdown形式に変換するツールをGASを使ってWebApp化した

まぐろたべたい。

ポキオ GooglePhotoMarkdowner Online

もうWebAppでよくね?

以前作成したElectronアプリがありまして。

relativelayout.hatenablog.com

これは、GooglePhotosにアップした写真への直リンクを取得し、Markdownなドキュメントに貼り付けられるように変換するツールです。

このアプリは、GooglePhotos上の画像をリンクで共有した際に、そのリンク先のページのOpen Graph Protocolで記載された画像パスを取得し、Markdown形式に変換するアプリです。要はHTTP-GETして、パースして、適当な文字列を付加するだけの単純なアプリです。

もともとElectronで実装していたものの、デスクトップアプリでなくてもいいようなレベルの実装内容なのと、最近地味に使っているChrome OSでも使いたくなった(Electronが動かない)ため、今回はWebApp化を行ってみました。

WebAppをGASで超絶簡単に作成する

GASは光速でWebAPIが無料で作れることに定評がありました。

relativelayout.hatenablog.com

WebAPIができるので、サーバーサイドの処理をWebAPIとして切り出してしまったり、WebAPIの返り値に静的なHTMLを返せばWebページのホスティングもできてしまうすごい子なんです。

今回はこんな感じで処理を書いていました。

  • GAS側で実装したのはmarkdowner.gsindex.html
  • UIや基本的な処理はindex.html記載(めんどくさかった)
  • ただしindex.html上で他のドメインにHTTP-GETを行うとCORSに引っかかってしまう
  • そこでmarkdowner.gs側にHTTP-GETをする機能を実装
  • index.htmlからgoogle.script.runを使ってmarkdowner.gsの処理を呼び出す

コードは例によって最後に記載しておきます。

これで念願のWebApp化完了

出来上がったものがこちら

ポキオ GooglePhotoMarkdowner Online

https://bit.ly/3dYB5Yd

これでブラウザさえあればブログ執筆の効率化ができそうです。一応WebAppのURLを公開しておきます、動作の保証はありませんがきっと動きます。

クソコードはこちら

markdowner.gs

function doGet() {
  return HtmlService.createHtmlOutputFromFile('index');
}

function convertUrl(url){
  var response = UrlFetchApp.fetch(url);
  return response.getContentText().match(/<meta property="og:image".*?>/g)[0].match(/http.*?=/g)[0].replace("=", "");
}

index.html

<!DOCTYPE html>
<html>

<head>
    <base target="_top">
    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
        integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
</head>

<body role="document">
    <nav class="navbar navbar-dark bg-primary">
        <span class="navbar-brand mb-0 h1" id="result">GooglePhotoMarkdowner Online</span>
    </nav>

    <form class="mx-2">
        <div class="form-group">
            <div class="row mt-2 align-items-end">
                <div class="col">
                    <label for="photoLink">URL</label>
                    <input class="form-control" type="url" id="photoLink" placeholder="Google Photoへの共有リンクを貼り付けてください">
                </div>
            </div>

            <div class="row mt-2">
                <div class="col-3">
                    <label for="photoSize">サイズ</label>
                    <input class="form-control" type="number" id="photoSize" placeholder="(オプション)">
                </div>

                <div class="col">
                    <label for="photoAlt">代替テキスト</label>
                    <input class="form-control" type="text" id="photoAlt" placeholder="(オプション)">
                </div>

                <div class="col">
                    <label for="photoTitle">画像タイトル</label>
                    <input class="form-control" type="text" id="photoTitle" placeholder="(オプション)">
                </div>
            </div>

            <div class="row justify-content-center mt-2">
                <div class="col-auto">
                    <button type="button" class="btn btn-primary btn-lg" id="start" onclick="convert()">変換</button>
                </div>
                <div class="col-auto">
                    <button type="button" class="btn btn-secondary btn-lg" id="clear" onclick="clearAll()">クリア</button>
                </div>
            </div>



            <div class="row mt-4 align-items-end">
                <div class="col">
                    <label for="photoLink">変換されたURL</label>
                    <input class="form-control" type="text" id="resultUrl">
                </div>
            </div>

            <div class="row mt-2 align-items-end">
                <div class="col">
                    <label for="photoLink">Markdown記法</label>
                    <input class="form-control" type="text" id="resultMarkdown">
                </div>
            </div>

            <div class="row mt-2 align-items-end">
                <div class="col">
                    <label for="photoLink">HTML記法</label>
                    <input class="form-control" type="text" id="resultHtml">
                </div>
            </div>
        </div>
    </form>

    <!-- Modal -->
    <div class="modal fade" id="processingModal" tabindex="-1" role="dialog" aria-labelledby="processingModalTitle"
        aria-hidden="true">
        <div class="modal-dialog modal-dialog-centered modal-sm" role="document">
            <div class="modal-content">
                <div class="modal-body">
                    変換中…
                </div>
            </div>
        </div>
    </div>

    <div class="modal fade" id="errorModal" tabindex="-1" role="dialog" aria-labelledby="errorTitle" aria-hidden="true">
        <div class="modal-dialog modal-dialog-centered modal-sm" role="document">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title modal-title-danger">エラー</h5>
                    <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                        <span aria-hidden="true">&times;</span>
                    </button>
                </div>
                <div class="modal-body">
                    変換できませんでした。
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-danger" onclick="$('#errorModal').modal('hide');">OK</button>
                </div>
            </div>
        </div>
    </div>

    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
        integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous">
        </script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.6/umd/popper.min.js"
        integrity="sha384-wHAiFfRlMFy6i5SRaxvfOCifBUQy1xHdJ/yoi7FRNXMRBu5WHdZYu1hA6ZOblgut" crossorigin="anonymous">
        </script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/js/bootstrap.min.js"
        integrity="sha384-B0UglyR+jN6CkvvICOB2joaf5I4l3gm9GU6Hc1og6Ls7i6U/mkkaduKaBhlAXv9k" crossorigin="anonymous">
        </script>

    <script>
        function pasteFromClipboard() {
            if (navigator.clipboard) {
                navigator.clipboard.readText()
                    .then(function (text) {
                        $("#photoLink")[0].value = text;
                    });
            }
        }

        function convert() {
            if (!$("#photoLink")[0].value) {
                $('#errorModal').modal('show');
                return;
            }
            $('#processingModal').modal('show');
            getOgImageUrl();
        }

        function getOgImageUrl() {
            google.script.run.withSuccessHandler(function (result) {
                var generatedUrl = generateResizedUrl(result);
                setAddressResult(generatedUrl);
                setMarkdownResult(generatedUrl);
                setHtmlResult(generatedUrl);
                $('#processingModal').modal('hide');
            }).convertUrl($("#photoLink")[0].value);
        }

        function generateResizedUrl(url) {
            var result = url;

            if ($("#photoSize")[0].value) {
                result += "=s" + $("#photoSize")[0].value;
            } else {
                result += "=s0";
            }

            return result;
        }

        function setAddressResult(result) {
            $("#resultUrl")[0].value = result;
        }

        function setMarkdownResult(result) {
            var markdownResult = "![" + $("#photoAlt")[0].value + "](" + result + " \"" + $("#photoTitle")[0].value + "\")";
            $("#resultMarkdown")[0].value = markdownResult;
        }

        function setHtmlResult(result) {
            var htmlResult = "<img src=\"" + result + "\" alt=\"" + $("#photoAlt")[0].value + "\" title=\"" + $("#photoTitle")[0].value + "\">";
            $("#resultHtml")[0].value = htmlResult;
        }

        function clearAll() {
            $("#photoLink")[0].value = "";
            $("#photoSize")[0].value = "";
            $("#photoAlt")[0].value = "";
            $("#photoTitle")[0].value = "";
        }

        function copyUrl() {
            if (navigator.clipboard) {
                navigator.clipboard.writeText($("#resultUrl")[0].value);
            }
        }

        function copyMarkdown() {
            if (navigator.clipboard) {
                navigator.clipboard.writeText($("#resultMarkdown")[0].value);
            }
        }

        function copyHtml() {
            if (navigator.clipboard) {
                navigator.clipboard.writeText($("#resultHtml")[0].value);
            }
        }
    </script>
</body>

</html>

「Androidのメモとか」は、Amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイト宣伝プログラムである、Amazonアソシエイト・プログラムの参加者です。

このブログは個人的なメモ書きであったり、考えを書く場所であります。執筆者の所属する団体や企業のコメントや意向とは無関係であります。また、このブログは必ずしも正しいことが書かれているとは限らず、誤字脱字や意図せず誤った情報を載せる場合がありえます。それが原因で読者が不利益を被ったとしても、執筆者はいかなる責任も負いません。ありがとうございます。