Log

いろいろ

2023年に書いた俺のおすすめ記事を教える

2024年は年始から暗い話題ばかりで気が滅入っちゃいますね。気を紛らわすために私のブログでも読みましょう。2023年のおすすめ記事ベスト4を紹介します。ついでに関連する記事も。


大阪行ってきたやで

mtzml.hatenablog.com

旅行記です。私のブログでは珍しい万人受けする内容となります。

2023年は日本を開拓しました。大阪・長野・青森・福島・熊本・福岡・石川・福岡・広島・愛知・石川・石川。 MVPは各地でスマホに給電し続けたChargeSPOTです。お世話になりました。未知の場所でもスマホがあれば何とかなる。異世界はスマートフォンとともに。

その他の旅行記はこちら。


ポケモンDX2

mtzml.hatenablog.com

2月はずっとポケモンをやっていました。

ポケモンの育成を楽にするためにマイコンを購入し、プログラムを書いて、最終的にランクバトルに挑戦する長編ストーリー。その第二回目の記事が「ポケモンDX2」です。ポケモンもマイコンも知らなくても大丈夫。溢れ出る疾走感で読み切ることができます。

全編はこちら。


IntersectionObserverではてなブログ記事内のApple Musicを自動再生する

mtzml.hatenablog.com

2023年で一番伸びた記事です。が、プログラミング系の記事は前提知識がないと読んでもつまらんでしょう。私はハイコンテキストな事柄を好むのでこの類の記事はとても多いです。

といっても、この記事のように対象が分かりやすいもの・とっつき易いものも数多くあります。おもしろいアイディアを実装してエンターテイメントをお届けしたり、IT知識をゆるふわに説明したり、このテンション感の記事はおもしろく、かつ、ためになるのでおすすめです。

そんなウキウキプログラミングシリーズはこちら。

もちろんガチガチな記事もあります。

後者は大ボリュームで時間も愛情もかけたのですが、あまりにもPVが少なくてシクシクです。


2023年好きな楽曲10選

mtzml.hatenablog.com

毎年恒例の。

ただし、今年は納得のいく文章を書くことができませんでした。飛び道具に頼る始末。なさけない。実質的に5選であると結論づけているように、実に半分の曲について情熱が足りませんでした。記事全体にストーリーを持たせることができればよかったのですが結局グダグダになっています。

一方で、公開の早さについては自分を褒め散らかしたいと思います。楽曲10選を書いている人の中ではかなり早く書き終えたのではないでしょうか。何事もスピード感を大事にしているので、まとまりがなくグダグダでも早く公開するという決断をした自分を尊重します。

2023年に関連記事はありません。2022年の自分が納得のいく記事を載せておきます。*1


おわり

おすすめしてきた記事を含め2023年は38本の記事を書きました。

7月👊

2024年も同じくらい書けたらいいな。次は「2023年楽曲10選まとめ」の分析か技術構成について書くつもりです。お楽しみに。

楽曲10選まとめ

(10選記事を募集しています。)

*1:Distortion!! - LogDistortion‼ - Logは異なる記事です。

2023年好きな楽曲10選

2023年に発売された楽曲の中から好きな10曲を選びます。

今年は日本各地を旅しました。文字通り世界が広がったことで、歌詞の意味を多角的に捉えられるようなったと思います。ポッドキャストを始めるという挑戦をした年でもありました。その収録・編集作業を通じて音に対する解像度が上がったように感じます。

仕事の面では転職という一大イベントがありました。転職活動を支えてくれた楽曲には感謝しかありません。現在は有給消化期間のため労働の束縛から解き放たれています。

そんな自由を手に入れた今年のテーマは「自由な10選」としましょう。


(2023/12/24:追記)
まえがきを変更しました。

(2023/12/23:追記)
まえがきの追記と文章全体の微修正をおこないました。


季節のカルテット / 鈴木みのり

作詞:竹内サティフォ
作曲・編曲:ONIGAWARA

季節のカルテット

季節のカルテット

キャッチーでハッピーなポップチューン。カルテットは春夏秋冬を意味する。日本には四季がある。

2023年の晩冬、「季節のカルテット♪カルテット♪」というフレーズが私の中でバズり散らかしました。変な位置にアクセントがあるのか、不思議とクセになり、強烈に耳に残る。歌詞は違えどサビ中に四回もこのリズムが登場するので、1サビを聴き終わった頃には頭の中をカルテットが支配しています。思い返せば今年の2月はしばらく頭カルテットでした。(未確認の生命体?)

SNS言葉を多用した歌詞も特徴的です。

  • たった15秒で伝わる程 君は容易くないんだ
  • 君とじゃなきゃ 映えないな!
  • 100万回じゃ足りないくらい 二人の思い出バズらせよう!

2サビには100万ドルじゃ買えないくらいとあります。バズり指標としての100万回と「100万ドルの夜景」の100万という数字がよく揃ったなと感心していました。100万PV欲しいな。

欲が出てきたところでもっと強欲な歌詞を引用しましょう。

その髪も目も耳も唇も 話し方や笑い声も全部 フォルダーの中に君を捕まえて!

ええと。既視感のある歌詞なのだけれど。

最後はそれじゃまた春から始めよう!と締めくくります。「ずっと一緒」という想いを感じることができるきれいな終わり方です。


水彩世界 / スリーズブーケ

作詞:ケリー
作曲:栗原 暁(Jazzin’park)、前田 佑
編曲:前田 佑
弦編曲:門脇大輔

水彩世界

水彩世界

  • スリーズブーケ
  • アニメ
  • ¥255

春には蓮ノ空女学院スクールアイドルクラブが始動しました。*1

春風に舞う桜色 夏色叫んだ日の青 秋の暮れ燃える赤 白銀の冬

日本には四季がある。

私は蓮ノ空女学院スクールアイドルクラブ デビューミニアルバム『Dream Believers』過激派。その収録曲であることで+100万点。スリーズブーケが歌っていることで+100万点。

ライブでは毎回低音がイカれていて爆笑した想い出深い曲です。


フォーチュンムービー / スリーズブーケ

作詞:ケリー
作曲:桃宇アリサ、めんま
編曲:めんま

フォーチュンムービー

フォーチュンムービー

  • スリーズブーケ
  • アニメ
  • ¥255

低音の次は高音の話をしましょう。

今話題のスクールアイドル「スリーズブーケ」の良いところは何だと思いますか。正解は良い曲しか歌わないところです。次いで高音のユニゾンが気持ち良すぎるところ。声の相性が良いのでしょう。歌詞やメロディに切なさを添えてくれる大好きなボーカルです。

6月に参加したライブの感想を引用します。

ド!ド!ド!ド!ド!ド!ド!ド!ド! - Log

フォーチュンムービーという知らん曲はとても良かったので、帰ったら聴き込むます。この裏声でしか得られない成分がきっとある。

初見でも印象に残るくらい気持ち良かったので選出。心のビタミン不足に効く成分がありました。


Mix shake!! / スリーズブーケ

作詞:ケリー
作曲・編曲:川崎智哉

Mix shake!!

Mix shake!!

  • スリーズブーケ
  • アニメ
  • ¥255

スリーズブーケが誇る栄養満点ドカ沸き曲。曲名を元にした愉快な歌詞が元気を与えてくれます。

自分のことをスリーズブーケだと勘違いしているギターがBメロを歌うところから曲が始まります。そこからギアを上げてカッティングが心地よいイントロへ。歌いたくなるイントロ。曲はイントロが9割。

間奏はシンセの独壇場。電子音だとラスサビ前のレベルアップ音もいいね。サビではっちゃけるドラム。随所で動き回るベース。とにかく楽器隊がはちゃめちゃわちゃわちゃいい感じの曲です。

そろそろ文字を読むのにも飽きたでしょう。Mix shake!!をキメる動画を挟みます。

この後のベースが効きどころであり聴きどころ。

ライブではラスサビで他ユニット全員が出てきてタオルをぶん回す一幕も。異次元フェスではコラボで披露されました。演者も観客もみんな楽しむことができるライブ映えする曲ですね。


Reflection in the mirror / スリーズブーケ

作詞:ケリー
作曲:桃宇アリサ、めんま
編曲:めんま
弦編曲:真部 裕

Reflection in the mirror

Reflection in the mirror

  • スリーズブーケ
  • アニメ
  • ¥255


Holiday∞Holiday / スリーズブーケ

作詞:ケリー
作曲:桃宇アリサ、めんま
編曲:めんま

Holiday∞Holiday

Holiday∞Holiday

  • スリーズブーケ
  • アニメ
  • ¥255

君と過ごす毎日が休日のように楽しい。キラキラした音が全体を彩るかわいい曲です。

単独公演でもフェスでも毎日のように歌っていたスリーズブーケの代表曲であり、今年はライブで8回も聴きました。水樹奈々さんの夏ツアーで一番聴いた曲でも8回。実質Astrogationであると言えそうです。

たくさんライブで聴いたおかげで「はい!はい!」というスリーズブーケ二人の煽りが原曲でも聴こえてきます。この声色がとてもかわいい。かなりの推しポイントですがそれを伝えられないことが残念です。365日いつでも歌うので来年開催される蓮ノ空2ndライブツアーの配信を観ることを勧めます。私が参加できないと困るので現地には絶対に来ないでください。

急上昇して急旋回 急下降して急展開 全速力だよね毎回

単純だけどここの韻踏みも好きなポイントです。あと、隣でウキウキしている梢センパイを見ると笑っちゃいます。


Genyo 耀
Yako
スリーズブーケ

作詞:ケリー
作曲:小野寺祐輔(Arte Refact
編曲:脇 眞富(Arte Refact

眩耀夜行

眩耀夜行

  • スリーズブーケ
  • アニメ
  • ¥255

夏夜のストーリーを匂わせる幻想的な曲。

歌詞には街中が空を余所見する間になど聴き手に情景を思い浮かばせるような表現、詩的な表現が多く登場します。ラスサビ前は「きれいな夜だね」文学少女の日野下花帆さんが呟く。令和の夏目漱石である。*2早く紙幣になればいいのにな。最後はずっと…忘れないでとアウトロもなく終わります。代わりに数秒の無音、余韻がある。本当に小説を読んでいる気分になります。

好きな場面だから差し込んだだけで意味はない
(103期7月度Fes×LIVEより)

もちろん小説ではないので旋律があり声の表現も乗ります。二回歌われるこの手はもう離さないもそれぞれ異なる印象を受けることでしょう。曲中の最高音だと思いますが裏声ではない力強い歌声に決意を感じる。

主張の強いギターソロも印象的。ギターの歪みが気持ち良いというより少し浮いているように感じるけれど、この疾走感が駆け行く二人の様子を思わせてくれます。サビは盛り上げまくるブラスと美しいストリングスにより壮大さが際立つ。とくにブラスの明るさに引っ張られないところに編曲の妙を感じました。

曲そのものから逸れますが眩耀夜行の衣装がとても好きです。色鮮やかな衣装も曲の雰囲気を形作ります。


素顔のピクセル / スリーズブーケ

作詞:ケリー
作曲:桃宇アリサ、めんま
編曲:めんま

素顔のピクセル

素顔のピクセル

  • スリーズブーケ
  • アニメ
  • ¥255

今この瞬間の積み重ねで眩しい未来がある。特別なことだけじゃなくて、素顔の、日常の積み重ねの先に未来がある。今を楽しく大事に生きようと思わせてくれる曲です。

  • 歌いたくなるギターリフ
  • かわいいダンス
  • メッセージ性のある歌詞

この三つの要素から素顔のピクセルは構成されています。言及すべきはやはり歌詞でしょう。曲は歌詞が9割。

写真に関するワードが登場しますが、実際に写真を撮る訳で無いことはもちろん、過去を振り返るものでもありません。ジグソーパズルの一欠片やワンピクセルといった小さな部分が全体を織り成す。その全体もまた一つの部分となる。そのように今この瞬間の繋ぎ合わせが連続して未来がつくられる。余白だらけの一日もただ過ぎて行く一秒も、どんな感情もどんな場面も、今を大事に未来へ繋げて永遠にしよう。

楽しいも大切も大好きも そこにある一瞬を永遠にしよう 何一つ無駄じゃない ワンピクセルのかけがえの無い今

相対的に小さくなっていく一瞬も何一つ無駄じゃないと歌い切ってくれる点は頼もしい。パンチラインに続くサビ後半では具体的な内容が示されます。ここはコンテンツを知っていると解像度が上がって情景が鮮明に浮かぶことでしょう。*3

背を伸ばし凜とした横顔 照れ隠しそっぽ向いた顔も こぼさずに切り取ってくよ この眼はいつも君を追いかけている ピース!

無邪気かわいい花帆ちゃんと藍染の帳を下ろす乙宗
(103期9月度Fes×LIVEより)

ここまではコンテンツの文脈に沿って話してきましたが、この曲に陶酔している一番の理由は自分の考え方に近いところがあるからです。点が線に、線が面になって物事の理解が深まる。視点・視座を変えることでいろんな側面から気づきを得る。そうして世の中に対する解像度を上げて知的好奇心を満たすことが人生のおもしろさ。

こじつけではないですよ。スリーズブーケは蓮ノ空の伝統的なユニットであり、素顔のピクセルは歌い継がれる伝統曲です。私がスリーズブーケになったらそういう思いを込めて歌うというだけ。


アイデンティティ / みらくらぱーく!

作詞:ろさ
作曲・編曲:大熊淳生(Arte Refact

アイデンティティ

アイデンティティ

  • みらくらぱーく!
  • アニメ
  • ¥255

みらくらぱーく!で一番良い曲はアイデンティティです。

蓮ノ空1stライブツアーでは新曲を聴かない謎の縛りを自分に課していたため半分近くが知らない曲でした。その中で一番印象に残った曲がアイデンティティ。自分の理想を信じて追い求める様がハロめぐに重なり心にグッと来る。そしてやっぱ楽しく!というみらくらぱーく!らしい歌詞に惚れました。

ライブが終わった後も原曲を聴いていません。執筆時点で再生回数は5回未満。*4その時の感情を上書きしないよう、年内は聴き込まず、もう少し記憶に刻まれるのを待ちます。記録より記憶に残る曲でした。


ド!ド!ド! / みらくらぱーく!

作詞:ろさ
作曲・編曲:山本玲史

ド!ド!ド!

ド!ド!ド!

  • みらくらぱーく!
  • アニメ
  • ¥255

ド!ド!ド!はPROTECTIONだから好き。

ド!ド!ド!を聴くと泣いてしまう。蓮ノ空のこと好き好きクラブのみなさんであれば「それな!二人が困難を乗り越えてみらくらぱーく!を結成するエモ曲になったよね〜😭」と共感してくれることでしょう。

全然違う。俺は春から涙を流していた。

1サビと2サビを引用します。

ド!ド!ド!ドンと来いだよ ハンデ有りのノンフィクション いまいまいまこの瞬間が スタート切るチャンスなんだ ドンと来いだよ 気合い入れてこんちくしょう ほらほらもうあっちゅうま 大逆転しちゃうもんね

ド!ド!ド!ドン詰まりこそ 無敵になるモチベーション きたきたきたこの感覚が まだ頑張れる合図なんだ ドン詰まりこそ 笑顔見せてなんのその まだまだまだあれこんなもん 大成功しちゃうもんね

あっちゅうまこんちくしょうといった口語的な歌詞が好き。でも、一番のポイントは大逆転しちゃうもんねだ。そう、今は負けてるの。けど、気合い入れてこんちくしょう。ドン詰まりこそ無敵になるモチベーション。ドン詰まりこそ笑顔見せて何のその。勝ち組の目がまだあるから。


アウトロ

プレイリスト。

蓮まみれ、スリーズブーケまみれとなってしまい申し訳ございません。選曲が一つのコンテンツに偏ることが悪いとは思いません。ただ、自分の知らないコンテンツの曲だらけだと「うーん」と感じちゃうかなって。オタクは斜に構える生き物だから流行り物だとなおさらね。私は私の10選を楽しみにする方々がいると思っているので、その期待に応えられなかったのであれば申し訳ないです。

選曲が偏った原因は曲を掘れていないことです。一つのコンテンツにハマり過ぎたことで広く曲を聴くことができず、その結果、観測範囲にこの10曲を覆すものを見つけることができませんでした。さらに言えばこの10曲も無理やり選んでいます。これまで私の10選は楽曲間に優劣はありませんでした。しかし、今回は明らかに差があります。本文を読めば熱量の違いを感じることができるでしょう。以下が私の2023年楽曲10選あらため5選です。

最後に、10選を選ぶ上でコンテンツやアーティストに縛りを設けており困り果てているあなたへ。強者がルールを歪めることは現実にもあることです。スリーズブーケはゲームチェンジャー。せーのでその壁壊そう。

ラスサビとして動画を繰り返しました。*5この動画も100万年後には昔ばなしに載っけてください。あっ、また100万だ。

それでは。ぽちっ。

過去年度

*1:正確には我々が蓮ノ空女学院スクールアイドルクラブを発見しました。

*2:彼女はファンタジーを好みます。文学と括るのは強引かもしれません。

*3:伝統曲ですが今のスリーズブーケを思い浮かべます。後に述べる様に解釈は自由です。

*4:素顔のピクセルが500回再生であるため相対的にとても少ない数でしょう。

*5:このぶん殴る振りは公式の振りです。私は振りコピをしただけ。その箇所へのリンクを用意したのでよかったら見てね。【蓮ノ空バーチャルライブ全編無料公開】103期7月度Fes×LIVE [2023/07/31] (ラブライブ!蓮ノ空女学院スクールアイドルクラブ) #Fes蓮ノ空 - YouTube

AWS BackupでS3バケットのバックアップを設定するときはs3:GetBucketVersioningが必要

AWS Backupのリソース割り当てを設定している時の話。S3の対象となるバケットを指定して「リソースを割り当てる」ボタンを押しても何も起こらない。画面の遷移もなく、エラーメッセージも表示されない。

へんじがないただのしかばねのようだ

開発者ツールを確認すると403エラーを吐いていました。

403 Forbidden

AWS Backupの設定をおこなうユーザーまたはロールに対してs3:GetBucketLocationだけでなくs3:GetBucketVersioningも許可する必要があったみたいです。実際にバックアップを取得するロールではなく、バックアップの設定をおこなうロールまたはユーザーに権限が必要な点は注意かも。

メモ

そもそもバケットに対してアクセスを制限していない場合は問題ありません。バケットポリシーでアクセスを制限している場合に問題が生じます。例えば、社内環境からDirect Connect、Private Linkを経由してS3へアクセスするユースケースなど。サンプルのバケットポリシーを示します。

※本記事でサンプルとして記載するバケットポリシーは今回の事象を説明するためのものです。Actionの範囲が広すぎるとか、分割されたStatementで漏れがあるとか、そのようなことは一切気にしません。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Statement1",
            "Effect": "Deny",
            "Principal": "*",
            "Action": "s3:*",
            "Resource": [
                "arn:aws:s3:::mtzml-20231208",
                "arn:aws:s3:::mtzml-20231208/*"
            ],
            "Condition": {
                "StringNotEquals": {
                    "aws:SourceVpce": "vpce-12345678901234567"
                }
            }
        }
    ]
}

このバケットポリシーは特定のVPCエンドポイントを通した操作のみを許可します。

AWS Backupを利用する場合は、当然ながらバックアップを取得するロールにも操作を許可する必要があります。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Statement1",
            "Effect": "Deny",
            "Principal": "*",
            "Action": "s3:*",
            "Resource": [
                "arn:aws:s3:::mtzml-20231208",
                "arn:aws:s3:::mtzml-20231208/*"
            ],
            "Condition": {
                "StringNotLike": {
                    "aws:userid": "XXXXXXXXXXXXXXXXXXXXX:*"
                },
                "StringNotEquals": {
                    "aws:SourceVpce": "vpce-12345678901234567"
                }
            }
        }
    ]
}

バックアップの設定をおこなうロールと取得をおこなうロールが同一であれば、このバケットポリシーで問題ありません。設定と取得の主体が異なる場合はこのバケットポリシーでは不十分です。例えば、各個人に発行されたIAMユーザーを用いてマネジメントコンソールからバックアップ設定をおこなう場合、IAMユーザーに対象のバケットを閲覧できる権限がないと、そもそもリソース割り当ての選択肢に対象のバケットが表示されません。

「mtzml-20231208」が表示されない

IAMユーザーを特定する何かしらの条件でs3:GetBucketLocationを許可します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Statement1",
            "Effect": "Deny",
            "Principal": "*",
            "NotAction": "s3:GetBucketLocation",
            "Resource": [
                "arn:aws:s3:::mtzml-20231208",
                "arn:aws:s3:::mtzml-20231208/*"
            ],
            "Condition": {
                "StringNotLike": {
                    "aws:userid": "XXXXXXXXXXXXXXXXXXXXX:*"
                },
                "StringNotEquals": {
                    "aws:SourceVpce": "vpce-12345678901234567"
                }
            }
        },
        {
            "Sid": "Statement2",
            "Effect": "Deny",
            "Principal": "*",
            "Action": "s3:GetBucketLocation",
            "Resource": "arn:aws:s3:::mtzml-20231208",
            "Condition": {
                "StringNotEquals": {
                    "aws:PrincipalAccount": "012345678901"
                }
            }
        }
    ]
}

「20231208」が表示される

これでリソース割り当ての対象としたいバケットが選択できるようになりました。

しかし、ここで冒頭の事象が発生します。「リソースを割り当てる」ボタンを押しても画面上は何も起こりません。

へんじがないただのしかばねのようだ2

開発者ツールを確認すると403エラーを吐いているHTTPリクエストが存在します。

403 Forbidden

エラーとなっているHTTPリクエストのクエリパラメータが?versioningなので、おそらくバケットのバージョニングに関するリクエストなのでしょう。

AWS BackupでS3のバケットのバックアップを取得するためには、対象となるバケットのバージョンニングを有効にする必要があります。

Using AWS Backup for Amazon S3 - Amazon Simple Storage Service

Prerequisites
You must activate S3 Versioning on your bucket before AWS Backup can back it up.

今回のケースでは、リソース割り当て処理実行時に、バージョニングが有効であるか確認しようとして権限不足によりエラーが発生したと予測できます。

バケットのバージョニング情報を取得できるように、バックアップの設定をおこなうIAMユーザーに対してs3:GetBucketVersioningも許可します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Statement1",
            "Effect": "Deny",
            "Principal": "*",
            "NotAction": [
                "s3:GetBucketLocation",
                "s3:GetBucketVersioning"
            ],
            "Resource": [
                "arn:aws:s3:::mtzml-20231208",
                "arn:aws:s3:::mtzml-20231208/*"
            ],
            "Condition": {
                "StringNotLike": {
                    "aws:userid": "XXXXXXXXXXXXXXXXXXXXX:*"
                },
                "StringNotEquals": {
                    "aws:SourceVpce": "vpce-12345678901234567"
                }
            }
        },
        {
            "Sid": "Statement2",
            "Effect": "Deny",
            "Principal": "*",
            "Action": [
                "s3:GetBucketLocation",
                "s3:GetBucketVersioning"
            ],
            "Resource": "arn:aws:s3:::mtzml-20231208",
            "Condition": {
                "StringNotEquals": {
                    "aws:PrincipalAccount": "012345678901"
                }
            }
        }
    ]
}

うまくいった。

「ラブライブ!蓮ノ空女学院スクールアイドルクラブ 1st Live Tour ~RUN!CAN!FUN!~」感想

2ヶ月前の私の記事です。

ツアーの感想はツアー中に書け - Log

記憶が新しいうちに書かないと忘れちゃうので自分への戒めです。

って、ちが〜〜〜〜〜〜〜う!そんな話ではない!自分ではなく君に向けて言っている。

感想ブログを読んで「え、そうだったんだ」「なるほど、そういう視点もあるのか」と思ってももう遅いんですよ。次の公演がナイ…🥺

だから、ツアーの感想はツアー中に書いてください。よろしくお願いします。

アホめぐ。

こんな記事を書いたのに自分がツアー中に感想を書かないなんて道理が通らないでしょう。2023年11月現在、「ラブライブ!蓮ノ空女学院スクールアイドルクラブ 1st Live Tour ~RUN!CAN!FUN!~」が絶賛開催中です。私は蓮ノ空のこと好き好きクラブのみなさんでは無いのだけれど、たまたま福岡公演両日と東京公演両日に参加したので感想を記します。

ラブライブ!蓮ノ空女学院スクールアイドルクラブ 1st Live Tour ~RUN!CAN!FUN!~|LIVE&EVENT|「蓮ノ空女学院スクールアイドルクラブ」公式サイト

総括

曲ごとの話に先行して簡単に全体のまとめ。

  • ダンスのシンクロに感銘を受ける
    Fes×LIVEで披露した楽曲はモニターにその映像も映し出されます。キャプチャーを元にした映像なのでそれはそうという話だけど、キャラクターとキャストのシンクロしたダンスには感銘を受けます。ここは蓮ノ空のアイデンティティであり見どころです。
  • 喋らん、歌う
    ライブなので歌がメインとなっていてよかった。幕間映像にも時間を割いていましたが、この半年間の蓮ノ空の歩みを振り返るというコンセプトがあるのでこれはよい時間の使い方です。
  • 入場が楽
    これまでのライブでも思いましたが、顔認証で入場できるシステムはとても便利です。顔面を忘れなければ入場できる。

セットリスト

公式を見てください。

以下、福岡公演のセットリストをベースにして個別に曲の感想を記します。東京公演の入れ替え曲をねじ込んだりと順番ははちゃめちゃわちゃわちゃですがご了承ください。

Dream Believers

OPとして完璧な曲です。

完璧なのでとくに言及することはありません。一つ好きな点を上げると、1サビ手前で横に移動するハロめぐが好きです。自由に駆け出すよと駆け寄ってくる楡井希実さんも好きです。Dream Believersが好きです。

2023/11/20 (月) の「Link!Like!ラブライブ!」ハーフアニバーサリー記念日にはFes×LIVE verのリリックビデオが公開されました。

youtu.be

集大成感のあるリリックビデオなので愛知公演で映像が差し代わることはないと思いますが、私はダブルアンコールDream Believersが発生してこのリリックビデオが流れることを期待しています。Dream Believersで始まりDream Believersで終わるライブしか好きじゃない。

Yup! Yup! Yup!

知らない曲です。

前提として、私はアルバム「夏めきペイン」を聴いていません。他にも直近で知らない曲がたくさんあります。「知らない曲」とは文字通り知らないだけであり、その曲を邪険にあつかうものではありません。

ダンスに目をやると2サビのYup! Yup! Yup!では斜めにスライドしながら移動していました。東京公演で横から見て気づいたのですが、このスライドがきれいに一直線に揃っており、練習の跡を感じられてよかったです。それと、大サビは斜めではなくV字であることに気づいた。ヴィクトリー!(大沢瑠璃乃さんの語彙にありそう)

直後にMCがあるため他の曲に比べて言及されることが多く、公演を重ねるごとに情報が開示されていきました。

  • 曲冒頭では背中のネジを巻いている
  • 曲冒頭では「○」「×」「△」「□」を書いている
  • サビでは「Y」「U」「P」の文字を書いている

どうしてネジを巻いているか分からないので、ツアーが終わったらちゃんと曲を聴いてみようと思います。こうなったら意地でもツアー中はアルバムを聴かないのだ。

MC

私たちのテンションはここからーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー(疲れて歩くハロめぐ)ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーここまで!

東京公演Day2の楡井希実さんがナイスMCでした。

当日は花宮初奈さんが喉の不調により歌唱を控えての出演となることが発表されていました。MCで楡井希実さんは「ただでさえダンスが上手い先輩が今日はダンスに極振りとなる。後輩として置いていかれないようにがんばる」と。場を暗くさせない、ポジティブで素晴らしいMCでした。

本記事では楡井希実さんを贔屓します。

幕間映像

アルバムをめくるように、思い出を振り返るように、これまでの軌跡が映像に映し出されます。ここの幕間映像ではDEEPNESSまで。

最初は正直「うーん」といった感想でした。半年間の歩みを振り返るというツアーのコンセプト上、コンテンツとしてはよいものです。しかし、ユニット結成後に「私たちはスリーズブーケです!」と名乗り上げてBGMが流れ出したにも関わらず、歌わずに次のストーリーに進んでしまう。福岡公演Day1では期待して立ち上がったオタクも苦笑いをしていました。

だがしかし!東京公演では改善されました。まず、期待感を煽るBGMが無くなりました。そして、この次のみらくらぱーく!結成の幕間映像にはなりますが、こちらはストーリーからシームレスに曲へ移行するように構成が変更されました。成長に感動して泣いてしまった。アンケートなどのフィードバックには積極的に協力していこう。

水彩世界

前回のライブでも異変を感じていましたが、低音がイカれていて笑ってしまいます。

東京公演から「千変万華」がアンコールに追加されました。水彩世界は入れ替え曲なので今のところ東京公演Day2のみになりますが、スリーズブーケとしての歌唱1曲目が水彩世界、締めが千変万華と美しい流れが実現します。

水彩世界が始解で、千変万華が卍解なのだと思います。

咲かせた花は一つでも 力を合わせて花束に __________水彩世界

強そう。

Reflection in the mirror

せーのでこの壁を壊してぶん殴る振りが好きです。

友人には「嘘つけ、ぶん殴る振りなんてあるか」と言われました。次に連番するときは貴様をぶん殴ってやるからな。

AWOKE

私はデビューミニアルバム「Dream Believers」過激派なので、そこに収録されている楽曲を歌うと無条件に喜びます。

パフォーマンスの観点でDOLLCHESTRAの二人は対照的でおもしろいです。野中ここなさんは表情豊かに顔面で歌っていますが、佐々木琴子さんは一定の表情を保っていると感じます。*1

Sparkly Spot

当社比、ハモリがうまくなってきたと感じました。

今は上手くいかないことでも成長すればよい。それを追っていくことでライブ感を得る側面もあり、蓮ノ空という文脈ではこれも一興、ご愛嬌。

サビ追い越しての腕ぐるんが良いです。回していきましょう。

謳歌爛漫

スリーズブーケの高音ユニゾンでしか得られないビタミンがある。

これはその時々によるところかもしれませんが、原曲よりも楡井希実さんの声色がキュートに聴こえます。ミスマッチと捉えることもできますが、ただ切ないだけではない印象を受けとることができて私は好きです。

youtu.be

前に進むためでキリッとする日野下花帆さんがかわいい。

Holiday∞Holiday

急上昇して急展開する日野下花帆さんは無邪気かわいい。横でウキウキ揺れている乙宗 梢さんもかわいい。

何かに捕まってなくちゃ 振り落とされてしまいそう 安全バーの君の腕 ぎゅっと握りしめた

ここ、オタク的には視力が上がるところかもしれませんが、個人的には楽曲への解像度が上がるところです。

  • 何かに捕まってなくちゃで楡井希実さんが花宮初奈さんの手をとって花道を歩き出す
  • ぎゅっと握りしめたで花宮初奈さんが楡井希実さんの腕を握りしめる

前者、「君」から手をとってくれたことを知り得ただけでも、来てよかった、蓮ノ空ライブ!

Holiday∞Holidayの衣装を着ていたこともあり、手をとって花道を歩く様は完全にCDジャケットでした。というかこのCDジャケットめっちゃよい。

youtu.be

最後に揺れている日野下花帆さん・楡井希実さんがかわいい。

スケイプゴート

↑書き過ぎたので休憩します。

今回はどう歌い上げるのかと野中ここなさんに注目していました。

Tragic Drops

楽曲にあわせたライティングがよい。

DEEPNESS

ストーリー上で難しい曲と位置付けられていますが、それに違わぬ風格が出ていてカッケーです。

4人の中では相対的に高身長となる花宮初奈さん・佐々木琴子さんがカッコよく映りました。欲を言えばDEEPNESSのパンツスタイル衣装でも見たいところ。

魅せ所である間奏のダンスでオタクが沸いていた点もよかったです。

幕間映像

さて、1回目の幕間映像でも触れましたが、福岡公演と東京公演で大きく変わったところです。

  • 福岡公演
    • 幕間映像(ルリエスケープ、みらくらぱーく!結成)
    • DOLLCHESTRA・スリーズブーケ歌唱
    • みらくらぱーく!歌唱
  • 東京公演
    • 幕間映像(ルリエスケープ)
    • DOLLCHESTRA・スリーズブーケ歌唱
    • 幕間映像(みらくらぱーく!結成)
    • みらくらぱーく!歌唱

神アプデ。ストーリーは特大イントロ。繰り返しにはなるけど、ストーリーからのシームレスな曲移行はテンションぶち上がるんだなって、ルリ思う。ゆえに、ルリあり。

東京公演のみらくらぱーく!結成の幕間映像は福岡公演より短くなっていました。ハロめぐの「ハロめぐー!」コールに対してオタクが「ハロめぐー!」するところが無くなってしまったのは残念です。

ジブンダイアリー

なぜかあまり記憶がない。本当に歌った?そういう曲だしはんなり聴いていた?

Mirage Voyage

指パッチンができないことが悔やまれる🤌

衣装がよいですね。サビでステップを踏む度に揺れており、躍動感を感じられました。

2サビ最後憧れもについて。がなりを入れるのかしゃくるのか、野中ここなさんがどう歌うかが聴きどころです。

眩耀夜行

金沢駅の鼓門前で歌ったFes×LIVEの映像がめっちゃ好きなんですよね。きれいな夜が過ぎる。そして衣装がよいですね2。

youtu.be

川沿いを下るところが好きです。ボーカルでは離さないの1回目と2回目の差分に注目。

生バンドが欲しいなと思う今日この頃。コンセプトと逸れるからそれは無いことなので、何かしらのフェスに出るときは常に警戒していたい。

Kawaii no susume

会場ドカ沸き。ド! ド! ド! と張るくらいドカ沸いていました。

福岡公演でスリーズブーケだけ単独トロッコが無かったことを不思議に思っていました。ココでその伏線を回収。トロッコで会場中にKawaiiをバラ撒いて回っていました。Life is fine 最高になた!!

ライブではなく曲自体の話になってしまいますが、この曲に存在していいギターソロではないだろとニチャァ…してまう。

BIG LOVE🫶

青春の輪郭

知らない曲です。トロッコ出動。

「DOLLCHESTRAにこういう曲が来たか」という意外性を感じました。

ユニットでトロッコを回る際はそれぞれ別のトロッコに乗って、片方は会場を右回り、もう片方は会場を左回りに乗り回します。近くに来たメンバーにあわせてラブライブレード!の色を変える習性がオタクにはあるようで、会場の右側左側できれいに色が分かれていてすごいなと思いました。

斜に構え過ぎて意図したものだとシラけることもありますが、偶発的な産物には素直になるしかないですね。俺もラブライブレード!を買おうかな。

パラレルダンサー

知らない曲です。

残陽

知らない曲です。

スリーズブーケは卍解曲よりポップでキュートな曲を私は好みますが、この曲はかなり耳に残りました。*2入れ替え曲だったので聴いた回数が少なく記憶は曖昧ですが、ギターリフが癖になる感じだったかと。

1番と2番をそれぞれソロで歌っているところも印象的でした。2番で暇を持て余した楡井希実さんの謎の動きが好きです。私の眼は楡井希実さんしか追いかけていなかったので花宮初奈さんがどうだったかは知りません。

会場は間奏後のバックハグで盛り上がっていました。

Dear my future

知らない曲です。

ド! ド! ド!

「このフォーメーションは!?」

会場の最大風速はココでした。全員が期待している様が伝わってきて、そして、その期待に応えるド! ド! ド! を見ることができて俺は嬉しいよ。

イントロのステップが癖になります。今年一番良い歌詞である気合い入れてこんちくちょうはボティランゲージでもこんちくしょうが表現されておりとってもこんちくしょうでやがりました。

2日目は謎の衣装で登場して謎でしたがハクチューアラモードの衣装でした。

ハクチューアラモード

知らない曲です。と言いたいところですが、前回のライブで聴いたので他のアルバム曲よりは認識している曲です。

東京公演からはMVが生えてきました。*3

youtu.be

気になっていることがあります。落ちサビで楽器隊の音がデカくなるのか、かなり前面に出てきているように聴こえるのですが、これは原曲もそうなのでしょうか?

幕間映像

Link!Like!ラブライブ!のカードスチルです。

夏めきペイン

Dream Believersで言うところのDream Believersでしょうか。この楽曲はFes×LIVEで歌唱されたのでいくらか存じ上げています。

演出でシャボン玉が撒き散らかされていたのですが、現代のシャボン玉はしぶといようで、最後の方まで生き残っている個体が存在しました。

全体を通しての所感ですが、大沢瑠璃乃さんは要所でソロを任されているなと。それを比較的安定して歌う菅 叶和さんに好感を抱きます。

Take It Over

知らない曲です。

アイデンティティ

知らない曲です。

楽しくというみらくらぱーく!を象徴するフレーズが入っていました。他に聞き取れた歌詞やメロディから察するに「エモい曲なのでは」と思ったのでツアーが終わったら聴き込みたいね。

「楽しい」というワード関連で一つ。ツアータイトルにある「RUN!CAN!FUN!」はそれぞれスリーズブーケ・DOLLCHESTRA・みらくらぱーく!とユニットを表しています。*4ユニットを意味する他にも、Rブロック・Cブロック・Fブロックとアリーナのブロック分けにも使われていると聞きました。知らなかった。名古屋はFブロックだったのですが、今からツアータイトルを「FUN!RUN!CAN!」に変更しませんか?

最後のMCで菅 叶和さんが「大勢の前で抱きつくのは恥ずかしい」と述べていました。

ココン東西

知らない曲です。トロッコ出勤。

福岡公演では世界観をあしらったデコレーションの歌詞を載せた映像でした。東京公演からはMVに差し変わりました。

youtu.be

これは会場で唯一座っていた私のみ体感したことですが、低音がすごくてケツから突き上げられました。多分この曲だったと思います。ケツでココン東西を感じました。

素顔のピクセル

知らなかった曲です。最近500回ほど聴きました✌️🧩*5

明日の空の僕たちへ

知らない曲です。

聴いていて「お前ら引退するんか」と思いました。

幕間映像

アンコールでモニターにキャラクターが出てきました。夏めきペインの衣装。メインMC?は公演ごとに交代制。

日野下花帆さんだったと思いますが、「これからも走っていくぜ」という話から「位置について、よーい、ドン!」というMCは感動すら覚える。実際にMCポジションからOn your markのポジションへと位置についていたりもする。

On your mark

今回のツアーに限らず、On your markはMCでのパワーアップがすごいです。MCはイントロ。

ダンス曲といえばDEEPNESSでしょうか。いいえ、On your markこそ真のダンス曲なのです。全曲ダンスしてるけど。

キャストがカッコつけている点が見どころの一つ。一番の見どころは大サビ菅 叶和さんのロングトーンでしょう。直後に映像で抜かれる楡井希実さんのキメ顔も印象に残っています。

2サビ頭の回せは全力で回せ!!

Legato

知らない曲です。

曲の構成がおもしろかった記憶。

永遠のEuphoria

EDとして完璧な楽曲です。

完璧なのでとくに言及することはありません。新しい発見としては、落ちサビ2年生組の歌唱中に1年生組がその後ろに隠れていて、ひょこっと出てくることに気づきました。落ちサビで前を向いている人間はあまりいないので今回やっと気づいた発見でした。複数公演に参加するとアングルをちょっと変えて見ることができる点がよいですね。

Dream Believersから始まり、永遠のEuphoriaで終わる。デビューミニアルバム「Dream Believers」過激派の俺もニッコリ。もう何も怖くない。かかって来い社会。

MC

ここまで書いて気づいたのですが、東京公演のアンコールで追加された曲の感想を忘れていました。言及したいこともありますが、既に10,000字近く書いており疲れ果てたので割愛させてください。

ばいめぐ〜


手動関連記事。

*1:それは歌唱にも表れていて、前者は抑揚のある歌い方、後者はのっぺりとした歌声に聴こえます。書き方から察することができるように、当初はそれを好ましく思っていませんでした。もっと個性が見たいという想いです。しかし、よくよく考えると「夕霧綴理さんはそういうキャラなのでは?」と、そもそも自分が夕霧綴理さんについて、DOLLCHESTRAについて、蓮ノ空について不勉強であることを気付かされました。早く蓮ノ空のこと好き好きクラブのみなさんにならなくては。

*2:ここでの卍解曲とは広義の卍解曲を指し、水彩世界も含まれます。具体的には水彩世界・謳歌爛漫・残陽・千変万華を指します。眩耀夜行が含まれない点に注意してください。眩耀夜行が卍解っぽいかを考えましょう。

*3:「虹夏ちゃんに似てるな。そうえいば虹夏ちゃんはかわいいな」と考えながら見ていました。

*4:【第8回】今回結構真面目なラジオだったよね? -かんかん&こなちのみらくら待機室ラジオ(蓮ノ空女学院スクールアイドルクラブ) - YouTube

*5:ピースでもあり、ピースでもあるのだ✌️🧩

Next.jsのWebアプリをPWA化してWeb Pushでユーザーに通知を送りつける

Distortion.fmをPWA (Progressive Web Apps) 化しました。

Distortion.fm

ホーム画面へインストールすることで独立したアプリのように利用できます。最新エピソード配信のお知らせをスマホの通知で受信することもできます。イメージはこんな感じ。

Web Push!

ここまでがユーザー向けのお話。ここからは実装についてのお話。リポジトリも添えておきます。

github.com

今さら?

PWAというと今さら感があるかもしれませんが、iOSでWeb Pushが利用できるようになったのが今さらなんですもの。

2023年3月27日にリリースされたiOS 16.4からWeb Pushが利用できるようになりました。

WebKit Features in Safari 16.4 | WebKit

iOS and iPadOS 16.4 add support for Web Push to web apps added to the Home Screen. Web Push makes it possible for web developers to send push notifications to their users through the use of Push API, Notifications API, and Service Workers.

2023年7月12日にリリースされたiOSChromeではPWAをインストールできます。これにより普段Chromeを利用しているユーザーに、PWAをインストールするために一時的にSafariを使ってもらう必要がなくなりました。

‎「Google Chrome - ウェブブラウザ」をApp Storeで *1

115.0.5790.84 2023年7月12日
Chrome をご利用いただきありがとうございます。このバージョンの新機能は以下のとおりです。
ホーム画面に URL やプログレッシブ ウェブアプリを追加できるようになりました。ウェブページまたはプログレッシブ ウェブアプリを開き、アドレスバーの共有アイコンをタップして [ホーム画面に追加] をタップします。

ということ。今さら時代が来たのだ。

さて、引用したiOS 16.4のリリースノートにキーワードが出揃っていますね。

  • web apps added to the Home Screen
  • Push API
  • Notifications API
  • Service Workers

これらを組み合わせて今回の目的を実現します。

ゴール

今回のゴールは以下の2点です。

  • 単独のアプリとしてWebアプリを利用できる
  • 最新エピソードの通知を受信できる

オフラインでの利用やパフォーマンスを考慮したキャッシュ戦略はスコープ外です。キャッシュは利用しません。*2

前提としてWebアプリはNext.jsで作られています。また、next-pwaは利用しません。

そしてこの記事はPWAおよびWeb Pushの動作原理を述べるものではありません。淡々と実装した内容について書いていきます。ブログのタイトル通りログです。

Add to Home Screen

まずはWebアプリをホーム画面に追加して単独のアプリとして利用できるようにします。

ホーム画面に追加 - プログレッシブウェブアプリ (PWA) | MDN

マニフェストファイルを作成して/public/manifest.jsonに配置します。

{
  "short_name": "Dfm",
  "name": "Distortion.fm",
  "icons": [
    {
      "src": "icon-192x192.png",
      "type": "image/png",
      "sizes": "192x192"
    },
    {
      "src": "icon-512x512.png",
      "type": "image/png",
      "sizes": "512x512"
    }
  ],
  "start_url": "/",
  "display": "standalone",
  "theme_color": "#ffffff",
  "background_color": "#ffffff"
}

/pages/_document.tsxマニフェストファイルを読み込みます。コードは必要な部分を抜粋。

<Head>
  <link rel="manifest" href="/manifest.json" />
</HEAD>

ホーム画面にインストールできるようにするための対応はこれだけです。

Web Push

最新エピソードの通知を受信できるようにするためにNotification APIとPush APIを利用します。ここを読みましょう。

通知とプッシュを利用して PWA を再エンゲージ可能にするには - プログレッシブウェブアプリ (PWA) | MDN

Notification API、Push APIという順序が理解しやすいです。以降はそんなもの無視して話を進めます。

構成図です。

構成図

Service Workerの登録

Service Workerの登録

/pages/_app.tsxでマウント時にService Workerを登録します。

useEffect(() => {
  try {
    regsterServiceWorkerOrThowError()
  } catch (error) {
    // マウント時はエラーメッセージを表示しない
  }
}, []);
/**
 * Service Workerを登録する
 * Service Workerが利用できない場合は例外を投げる
 */
export const regsterServiceWorkerOrThowError = (): void => {
  try {
    navigator.serviceWorker.register("/service-worker.js");
  } catch {
    throw new Error(MESSAGE_NOT_SUPPORT_WEB_PUSH);
  }
}

Service Workerは/public/service-wokrer.jsに配置しています。登録に関連するinstallイベントとactivateイベントを示します。

self.addEventListener("install", (event) => {
  // Service Workerの更新があった場合に即座にactivateする
  event.waitUntil(self.skipWaiting());
});

self.addEventListener("activate", (event) => {
  // 再読み込みを待たずにService Workerを有効にする
  // https://developer.mozilla.org/ja/docs/Web/API/Clients/claim
  event.waitUntil(clients.claim());
});

ここではService Workerの更新を即時反映するための処理をおこなっています。Service Workerはそのライフサイクルにより更新がすぐに反映されません。self.skipWaiting()clients.claim()を利用することで即時反映します。

ただし、これはライフサイクルが持つ目的とメリットから逸脱することに注意が必要です。ライフサイクルについては以下の記事に詳しい説明があります。ちなみに私は全部読んでいません。このことが原因でいつか痛い目にあうのかも。

Service Worker のライフサイクル  |  Articles  |  web.dev

Notification API

Notification API

ユーザーへ通知許可をリクエストします。

/**
 * プッシュ通知を購読する
 */
const subscribeNotifications = async () => {
  try {
    // ユーザーから通知の許可を得る
    const permission = await getNotificationPermissionOrThrowError();
    setNotificationIsGranted(permission === "granted");

    if (permission === "granted") {
      // 略
    }
  } catch (error) {
    alert(error);
  }
};
/**
 * ユーザーに通知許可をリクエストする
 * 通知が利用できない場合に例外を投げる
 */
export const getNotificationPermissionOrThrowError = async (): Promise<NotificationPermission> => {
  let permission: NotificationPermission;

  try {
    permission = await Notification.requestPermission();
  } catch {
    throw new Error(MESSAGE_NOT_SUPPORT_WEB_PUSH);
  }

  if (permission === "denied") {
    throw new Error(MESSAGE_DENIED_NOTIFICATIONS);
  }

  return permission;
}

Notification.requestPermission()を実行することでユーザーに通知許可を求めるダイアログが表示されます。おそらく印象がよくないであろうあのダイアログです。

PC

スマホ

人類はこれを許可してくれるのでしょうか。反射で拒否されそうです。拒否などのユーザーアクションの結果により以下の3つの値のいずれかに解決されます。

  • granted: 許可
  • denied: 拒否
  • default: 不明

ユーザーから許可を勝ち取ることができればデバイスの通知機能を利用できるようになります。

Push API

Push API

複雑になってきました。構成図のPush Serverは各ブラウザベンダが用意しているものです。

ここではweb-pushライブラリを利用します。

事前準備として秘密鍵と公開鍵を生成します。これはアプリケーションのコードではありません。ローカルPCで一度だけ実行してその結果を環境変数に保存します。

GitHub - web-push-libs/web-push: Web Push library for Node.js

const vapidKeys = webpush.generateVAPIDKeys();

// Prints 2 URL Safe Base64 Encoded Strings
console.log(vapidKeys.publicKey, vapidKeys.privateKey);

アプリ側の実装です。Notification APIで通知許可を得た場合にプッシュ通知を購読します。

/**
 * プッシュ通知を購読する
 */
const subscribeNotifications = async () => {
  try {
    // ユーザーから通知の許可を得る
    const permission = await getNotificationPermissionOrThrowError();
    setNotificationIsGranted(permission === "granted");

    if (permission === "granted") {
      // プッシュ通知をSubscribe
      const subscription = await subscribeWebPushOrThrowError();
      setSubscribing(true);

      // アイコンを揺らす
      setShakeIcon(true)

      // APIにSubscribe情報を渡してDBへ保存する
      await fetch("/api/subscription", {
        method: "POST",
        headers: {
          "Content-type": "application/json"
        },
        body: JSON.stringify({ subscription })
      });
    }
  } catch (error) {
    alert(error);
  }
};
/**
 * PushSubscriptionを購読する
 * WebPushが利用できない場合は例外を投げる
 */
export const subscribeWebPushOrThrowError = async (): Promise<PushSubscription> => {
  const vapidPublicKey = process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY;
  if (!vapidPublicKey) {
    throw new Error(MESSAGE_VAPID_PUBLIC_KEY_IS_NOT_FOUND);
  }

  try {
    const registration = await navigator.serviceWorker.ready;
    const subscription = await registration.pushManager.subscribe({
      userVisibleOnly: true,
      applicationServerKey: vapidPublicKey
    });
    return subscription;
  } catch {
    throw new Error(MESSAGE_NOT_SUPPORT_WEB_PUSH);
  }
}

メインとなる処理はここです。

const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
  userVisibleOnly: true,
  applicationServerKey: vapidPublicKey
});

登録されているService Workerを取得してプッシュ通知を購読します。事前準備で生成した公開鍵を引数として渡します。

MDNのサンプルではこの公開鍵をUint8Arrayに変換する処理が記載されていました。

通知とプッシュを利用して PWA を再エンゲージ可能にするには - プログレッシブウェブアプリ (PWA) | MDN

これはChromeのための対応ですが当該issueは既にクローズされています。よって特別な対応は不要と考えてUint8Arrayへの変換はおこなっていません。動作も問題なし。

802280 - chromium - An open-source project to help move the web forward. - Monorail

以上でプッシュ通知を購読することができました。

なお、購読の有効期限が切れたタイミングなのか以下のエラーが発生することがありました。

Web Push Error 410: the push subscription has expired or the user has unsubscribed

その場合は再度購読します。

ServiceWorkerGlobalScope: pushsubscriptionchange イベント - Web API | MDN

self.addEventListener("pushsubscriptionchange", (event) => {
  event.waitUntil(
    self.registration.pushManager
      .subscribe(event.oldSubscription.options)
      .then((subscription) =>
        fetch("/api/subscription", {
          method: "PUT",
          headers: {
            "Content-type": "application/json",
          },
          body: JSON.stringify({
            oldEndpoint: event.oldSubscription.endpoint,
            subscription,
          }),
        })
      )
  );
});

プッシュ通知を送る

プッシュ通知を送る

プッシュ通知の購読後はAPIをコールします。購読情報をDBに保存し、実際にプッシュ通知を利用してユーザーに処理の成功をお知らせします。

/pages/api/sunscription.tsから必要な部分だけ抜き出します。

const webPush = require("web-push");

webPush.setVapidDetails(
  "https://distortion.fm/",
  process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY,
  process.env.VAPID_PRIVATE_KEY
)

const postMethod = async (req: NextApiRequest, res: NextApiResponse) => {
  const subscription = {
    endpoint: req.body.subscription.endpoint,
    keys: req.body.subscription.keys
  }

  await webPush.sendNotification(
    subscription,
    JSON.stringify({
      title: "通知設定ON",
      body: "こんな感じで最新エピソードの追加をお知らせするよ。"
    })
  );

  // INSERT
  await sql`
    INSERT INTO subscription(endpoint, keys_p256dh, keys_auth)
    VALUES(${subscription.endpoint}, ${subscription.keys.p256dh}, ${subscription.keys.auth})
  `;

  res.status(201).end();
}

「Push API」で作成した秘密鍵と公開鍵をセットします。あとはsendNotification()メソッドを呼び出せばOK。

webPush.setVapidDetails(
  "https://distortion.fm/",
  process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY,
  process.env.VAPID_PRIVATE_KEY
)

最後はService Workerでプッシュ通知を受け取った際の処理を書きます。といってもユーザーに通知を表示するだけです。

self.addEventListener("push", (event) => {
  if (!event.data) return;
  const data = JSON.parse(event.data.text());

  event.waitUntil(
    self.registration.showNotification(
      data.title,
      {
        body: data.body,
        icon: "/favicon.ico",
        data: {
          url: data.url
        }
      }
    )
  );
});

self.addEventListener("notificationclick", (event) => {
  event.notification.close();
  event.waitUntil(
    clients.openWindow(event.notification.data?.url || "/")
  )
});

こんな感じで通知を受け取ることができます。

通知設定完了の通知を通知

最新エピソードをお知らせする

通知を飛ばせるようになったので最後に実際のユースケースを試します。最新エピソードの追加をユーザーにお知らせしましょう。

最新エピソードの追加を通知する

前回の記事で書いたとおりGoogle Colabで文字起こしをしています。

ポッドキャストを文字起こしして再生している文章をハイライト表示する - Log

文字起こし完了後にユーザーへの通知処理を追加します。前回よりJupyter Notebookの使い方が洗練されました。

セクションに分けてみた

前段の処理はリファクタしただけなので割愛して「プッシュ通知」のセクションだけを載せます。

!pip install git+https://github.com/web-push-libs/pywebpush.git
!pip install beautifulsoup4 psycopg2-binary requests
import json
from bs4 import BeautifulSoup
import psycopg2
from psycopg2.extras import DictCursor
from pywebpush import webpush, WebPushException
import requests


def get_episode_title(episode_id):
  """
  Webサイトからエピソードのタイトルを取得する
  """
  url = f"https://distortion.fm/episode/{episode_id}"
  html = requests.get(url)
  soup = BeautifulSoup(html.content, "html.parser")
  return soup.find("title").text


# 通知するdataを構築
data={
  "title": "新しいエピソードが視聴できます。",
  "body": get_episode_title(EPISODE_ID),
  "url": f"/episode/{EPISODE_ID}"
}


# 通知対象をDBから取得
with psycopg2.connect(DATABASE_URL) as conn:
  with conn.cursor(cursor_factory=DictCursor) as cur:
    cur.execute("SELECT endpoint, keys_p256dh, keys_auth FROM subscription")
    rows = cur.fetchall()


# 通知
for row in rows:
  print(row)

  subscription_info={
    "endpoint": row["endpoint"],
    "keys": {
      "p256dh": row["keys_p256dh"],
      "auth": row["keys_auth"]
    }
  }

  try:
    webpush(
      subscription_info=subscription_info,
      data=json.dumps(data),
      vapid_private_key=VAPID_PRIVATE_KEY,
      vapid_claims={
        "sub": EMAIL,
      }
    )
  except WebPushException as ex:
    print("I'm sorry, Dave, but I can't do that: {}", repr(ex))

「Push API」と同様にweb-pushライブラリを利用したいところですが、わざわざNode.jsを利用するのもあれかなと思ってPythonで書きました。pywebpushライブラリを利用します。毎度のことですが俺はふいんきPythonを書いている。そして俺はDaveではない。READEMEのコードを参考にしただけです。

GitHub - web-push-libs/pywebpush: Python Webpush Data encryption library

無事に通知が届きました。

500: Internal Server Error

は?*3

参考

*1:2023年11月13日時点の閲覧情報を引用しています。時間経過により過去のアップデート履歴は閲覧できなくなる可能性があります。

*2:Service Workerを利用することによりデフォルトでキャッシュされる仕組みなどがあるかもしれませんが、そういった仕組みがあるかも調べていません。能動的にキャッシュの設定をしていないというだけです。

*3:このエラーはおそらくSSGに起因するもので今回の主題とは関係ありません。