高木のブログ

Mac に Homebrew で pyenv をインストールする

Tags: Python pyenv

Mac にデフォルトで入っている Python が古かったので、pyenv で最新バージョンを入れることにした

$ python -V
Python 2.7.16
$ which python
/usr/bin/python

$ python3 -V
Python 3.7.9
$ which python3
/usr/local/bin/python3

手順 🔗

pyenv のインストール 🔗

$ brew install pyenv

$ pyenv -v
pyenv 2.3.15

pyenv のセットアップ 🔗

.zshrc に以下の内容を追記

export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"
$ source ~/.zshrc

Python のインストール 🔗

最新バージョンの Python をインストールして、デフォルトに設定

$ pyenv install 3.11.2
$ pyenv global 3.11.2

$ python -V
Python 3.11.2

【Bash】history + peco

history + peco で過去のコマンドを検索しやすいようにした

function peco_search_history() {
  local l=$(HISTTIMEFORMAT= history | \
  sort -r | sed -E s/^\ *[0-9]\+\ \+// | \
  peco --query "$READLINE_LINE")
  READLINE_LINE="$l"
  READLINE_POINT=${#l}
}
bind -x '"\C-r": peco_search_history'

Ctrl + R を上書きしている

重複したコマンドは履歴に残さない設定 もするとより良い

参考 🔗

【Bash+peco】ターミナルでのコマンド履歴検索を簡単に

【Python】Docker で FastAPI の開発環境を構築する

バージョン 🔗

  • Docker 20.10.21
  • Docker Compose 2.12.2
  • Python 3.11

手順 🔗

アプリのディレクトリ作成と移動 🔗

$ mkdir rails-app
$ cd rails-app

必要なファイルの準備 🔗

以下、4つのファイルを作成する

  • Dockerfile
  • compose.yml
  • main.py
  • requirements.txt
FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt

COPY . /app

CMD ["uvicorn", "main:app", "--reload", "--host", "0.0.0.0"]
services:
  web:
    build: .
    command: uvicorn main:app --reload --host 0.0.0.0
    volumes:
      - .:/app
    ports:
      - 8000:8000
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
    return { "message": "Hello World!" }
fastapi
uvicorn[standard]

ビルドと起動 🔗

$ docker compose build
$ docker compose up

確認 🔗

$ curl localhost:8000
{"message":"Hello World!"}

補足 🔗

__pycache__ というディレクトリが自動生成される
これはキャッシュを保存しておくためのディレクトリなので、Git 使う場合は除外対象にしておく

【M5Unified】サンプルコード(ボタン、加速度センサー・ジャイロセンサー編)

画面ありの M5Stack シリーズを M5Unified ライブラリを使っていろいろ試した際に生まれたコード群

GitHub に上げるほどでもないのでここに残す

今回は、ボタン、加速度センサー・ジャイロセンサー編

Arduino 言語(ほぼ C 言語)は書き慣れていないので参考程度に

インデントは半角スペース4つでいいのだろうか

ボタン 🔗

ボタンクリックで ON/OFF 切り替え 🔗

#include <M5Unified.h>

bool state = false;

void setup() {
    auto cfg = M5.config();
    cfg.clear_display = true;
    M5.begin(cfg);

    M5.Lcd.setTextSize(1);
    M5.Lcd.println("Click Button");
    print(state);
}

void loop() {
    M5.update();

    if (M5.BtnA.wasClicked()) {
        state = !state;
        print(state);
    }
}

void print(int state) {
    M5.Lcd.setTextSize(2);
    M5.Lcd.setCursor(0, 10);
    M5.Lcd.print(state ? "ON " : "OFF");
}

何連続クリックしたかカウント 🔗

#include <M5Unified.h>

void setup() {
    auto cfg = M5.config();
    cfg.clear_display = true;
    M5.begin(cfg);

    M5.Lcd.setTextSize(1);
    M5.Lcd.println("Click Button");
}

void loop() {
    M5.update();

    if (M5.BtnA.wasDeciedClickCount()) {
        M5.Lcd.setTextSize(2);
        M5.Lcd.setCursor(0, 10);
        M5.Lcd.printf("%d Clicked  ", M5.BtnA.getClickCount());
    }
}

ボタンクリックのカウントアップ、長押しでリセット 🔗

#include <M5Unified.h>

int count = 0;

void setup() {
    auto cfg = M5.config();
    cfg.clear_display = true;
    M5.begin(cfg);

    M5.Lcd.setTextSize(1);
    M5.Lcd.println("Please click button");
    print(count);
}

void loop() {
    M5.update();

    if (M5.BtnA.wasClicked()) {
        count++;
        print(count);
    }

    if (M5.BtnA.pressedFor(1000)) {
        count = 0;
        print(count);      
    }
}

void print(int count) {
    M5.Lcd.setTextSize(2);
    M5.Lcd.setCursor(0, 10);
    M5.Lcd.printf("Count: %d   ", count);
}

メニュー画面。ボタンクリックで選択切り替え、長押しで決定。もう1度長押しで選択し直し 🔗

#include <M5Unified.h>

int step = 1;
bool is_operation_locked = false;
char *menu[] = {"MenuA", "MenuB", "MenuC"};
int selected_menu_index = 0;

void setup() {
    auto cfg = M5.config();
    cfg.clear_display = true;
    M5.begin(cfg);

    print_step_1();
}

void loop() {
    M5.update();

    if (is_operation_locked) {
        if (M5.BtnA.wasReleased()) {
            is_operation_locked = false;
        }
        return;
    }

    if (step == 1) {
        if (M5.BtnA.wasClicked()) {
            selected_menu_index++;
            if (selected_menu_index >= 3) {
                selected_menu_index = 0;
            }
            print_select_menu();
        }

        if (M5.BtnA.pressedFor(1000)) {
            step = 2;
            is_operation_locked = true;
            print_step_2();
        }
    } else if(step == 2) {
        if (M5.BtnA.pressedFor(1000)) {
            step = 1;
            is_operation_locked = true;
            print_step_1();
        }
    }
}

void print_step_1() {
    M5.Lcd.clear();
    M5.Lcd.setTextSize(1);
    M5.Lcd.setCursor(0, 0);
    M5.Lcd.println("Please select menu");
    print_select_menu();
}

void print_step_2() {
    M5.Lcd.clear();
    M5.Lcd.setTextSize(1);
    M5.Lcd.setCursor(0, 0);
    M5.Lcd.println("Slected menu");
    print_selected_menu();
}

void print_select_menu() {
    M5.Lcd.setTextSize(2);
    M5.Lcd.setCursor(0, 10);
    for (int i = 0; i < 3; i++) {
        bool is_selected = i == selected_menu_index;
        M5.Lcd.printf("%s %s\n", is_selected ? ">" : " ", menu[i]);
    } 
}

void print_selected_menu() {
    M5.Lcd.setTextSize(2);
    M5.Lcd.setCursor(0, 10);
    M5.Lcd.printf("%s!!!", menu[selected_menu_index]);
}

加速度センサー、ジャイロセンサー 🔗

加速度とジャイロの表示、ボタンクリックで表示する画面切り替え 🔗

#include <M5Unified.h>

int state = 0;
float ax, ay, az, gx, gy, gz, t;

void setup() {
    auto cfg = M5.config();
    cfg.clear_display = true;
    M5.begin(cfg);
    M5.Imu.begin();
}
void loop() {
    M5.update();

    if (M5.BtnA.wasClicked()) {
        state = 1 - state;
    }

    if (state == 0) {
        M5.Display.setCursor(0, 0);
        M5.Lcd.setTextSize(1.5);

        M5.Imu.getAccel(&ax, &ay, &az);

        M5.Display.printf("ax: %3.1f \n", ax);
        M5.Display.printf("ay: %3.1f \n", ay);
        M5.Display.printf("az: %3.1f ", az);
    } else {
        M5.Display.setCursor(0, 0);
        M5.Lcd.setTextSize(1.5);

        M5.Imu.getGyro(&gx, &gy, &gz);

        M5.Display.printf("gx: %3.1f \n", gx);
        M5.Display.printf("gy: %3.1f \n", gy);
        M5.Display.printf("gz: %3.1f ", gz);
    }

    delay(100);
}

【history】重複したコマンドは履歴に残さない設定

Tags: コマンド

重複したコマンドは履歴に残さないようにしたい

手順 🔗

.bashrc とかに以下の設定を追記すればいい

HISTCONTROL=erasedups

設定を反映

$ source .bashrc

補足 🔗

挙動 🔗

履歴にコマンドを保存する時に同じコマンドの履歴をすべて削除するという挙動になっているので、設定をしたからといってすぐに重複カットされるわけではない

HISTCONTROL で設定できる項目 🔗

  • ignorespace: 空白文字で始まる行を保存しない
  • ignoredups: ひとつ前の履歴エントリと一致する行を保存しない
  • ignoreboth: ignorespace と ignoredups の省略形
  • erasedups: 現在の行と一致する履歴を保存前にすべて削除する

参考 🔗

【Hono】create-hono コマンド

Tags: Hono

Hono v3.0.0 から create-hono コマンドが追加された

create-react-app コマンドみたいもので、npm create hono@latest アプリ名 もしくは yarn create hono アプリ名 でサクッと Hono のアプリケーションを作成できる

$ yarn create hono sample-app

どのプラットフォームで使うか選べる

? Which template do you want to use? › - Use arrow-keys. Return to submit.
    bun
    cloudflare-pages
❯   cloudflare-workers
    deno
    fastly
    lagon
    nextjs
    nodejs

cloudflare-workers を選んだら、以下のファイル構成でアプリが作成された

$ cd sample-app
$ tree .
.
├── README.md
├── package.json
├── src
│   └── index.ts
├── tsconfig.json
└── wrangler.toml

1 directory, 5 files
$ yarn install
$ yarn dev
$ curl http://0.0.0.0:8787
Hello Hono!

今までの wrangler init して、yarn add hono する手順から解放されて良き

【Hono】v3.0.0 からバリデーションの書き方が変わった

Tags: Hono

Hono v3.0.0 がリリースされた!めでたい!

バリデーションの書き方が変わった 🔗

以前の Validator Middleware は廃止されて、薄いバリデーターしか提供されなくなった
サードパーティのバリデーターと組み合わせて使うと良いらしい
公式では Zod が推奨されていて、Zod Validator Middleware が用意されている

ちょうど手元に簡単なバリデーションを書いているものがあったので書き換えてみた

Before 🔗

import { Hono } from 'hono'
import { validator } from 'hono/validator'

const app = new Hono()

app.get(
  '/',
  validator((v) => ({
    tags: v.query('tags').isRequired().message('tags is a required parameter.'),
    associate_id: v.query('associate_id').isRequired().message('associate_id is a required parameter.'),
    count: v.query('count').isOptional().isNumeric().message('count must be numeric.'),
  })),
  async (c) => {
    const { tags, associate_id, count } = c.req.valid()
    // ...
  }
)

export default app

After 🔗

$ yarn add zod @hono/zod-validator
import { Hono } from 'hono'
import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'

const app = new Hono()

app.get(
  '/',
  zValidator(
    'query',
    z.object({
      tags: z.string({ required_error: 'tags is a required parameter.' }),
      associate_id: z.string({ required_error: 'associate_id is a required parameter.' }),
      count: z.number({ invalid_type_error: 'count must be numeric.' }).optional(),
    }),
    (result, c) => {
      if (!result.success) {
        return c.text(result.error.issues.map(i => i.message).join('\n'))
      }
    }
  ),
  async (c) => {
    const { tags, associate_id, count } = c.req.valid('query')
    // ...
  }
)

export default app

所感 🔗

  • Zod が使えるのが良い(以前も使おうと思えば使えたのかな?)
  • バリデーションエラー時のレスポンスを変えたくなくて、以下のコードを追加したため以前よりコードが長くなってしまったのが気になる
(result, c) => {
  if (!result.success) {
    return c.text(result.error.issues.map(i => i.message).join('\n'))
  }
}

他にシンプルな書き方がないか引き続き調査、もしくはレスポンスを変えてしまうかの検討をしたい

ChatGPT がすごい

Tags: ChatGPT OpenAI

今更だけど、ChatGPT にハマっている

凄いのは知っていたけど、実際に使ってみると改めて ChatGPT の凄さがわかった

実装で困った時に質問したらわりかしちゃんと返ってくる

正しい情報が必ず返ってくるとは限らないので補助的なツールとして今度も使っていきたい

ちなみに以下は、ChatGPT に「ChatGPTについてブログを書いて」と依頼して出力されたもの

ChatGPT とは何か 🔗

OpenAI が開発した自然言語処理のための大規模な言語モデル

GPT とは「Generative Pre-training Transformer」の略で、Transformer と呼ばれる機械学習のアルゴリズムを用いて、自然言語を生成することができる

ChatGPT の特徴 🔗

大量のデータを学習することで、自然言語を理解し、自然な対話を行うことができる

そのため、ChatGPT を利用することで、人間との対話や文章の生成などが可能となる

ChatGPT の活用例 🔗

自動応答システムやチャットボットを開発することができる

また、ニュース記事の要約や翻訳など、さまざまな自然言語処理のタスクにも活用されている

ChatGPT の将来性 🔗

今後も、より高度な自然言語処理技術が求められる中、ChatGPT はますます重要な存在となっていくだろう

また、AIと人間との対話がますます身近なものとなる中、ChatGPT が人工知能としての役割を果たすことも期待されている

Gatsby v5 は Cloudflare Pages ではまだ動かない

※ これは2023年2月11日現在の話

このブログは Gatsby で作って、Cloudflare Pages でホスティングしている

そろそろ Gatsby を v5 に上げようとしたら、Cloudflare Pages ではまだ動かないことが判明した

Gatsby v5 は Node.js 18 以上が必要で、Cloudflare Pages の Node.js 17 までしか対応していなかった

ということで、Gatsby のアップグレードは一旦保留になった

追記 (2023-04-19) 🔗

Cloudflare Pages で Node.js 18 がサポートされた ので、Gatsby v5 にアップグレードできるようになった

参考 🔗

ripgrep で隠しファイルも検索対象にする

問題 🔗

以下の隠しファイルがあった時

HOGE=12345

ripgrep で HOGE を検索してもヒットしない

$ rg HOGE
No files were searched, which means ripgrep probably applied a filter you didn't expect.
Running with --debug will show why files are being skipped.

解決方法 🔗

–hidden オプションを付けると隠しファイル、隠しディレクトリも検索できる

$ rg --hidden HOGE
.env
1:HOGE=12345

エイリアス 🔗

隠しファイルは常に検索対象にしたいし、都度オプションを付けるのは手間なのでエイリアスを設定した

alias rg="rg --hidden"

【Ruby】関数とメソッドの違い

Tags: Ruby

関数とメソッドの違いを聞かれて「同じでしょ」と答えてしまったけど、厳密には違いがあったのでここにメモしておく

関数とメソッドの違い 🔗

関数 🔗

何かしらの処理をひとまとめにしたもの
引数を受け取ったり、戻り値を返したりもする(もちろんなくても良い)

メソッド 🔗

オブジェクトに結びつけられた関数

Ruby における関数 🔗

全てがオブジェクトとなる Ruby では、メソッドと呼ぶのが正しい

puts は組み込み関数と思いきや、 Kernel モジュールに定義されているのでメソッドである

結論 🔗

  • Ruby (オブジェクト指向言語)において、関数とメソッドは厳密には違う
  • メソッドはオブジェクトに結びつけられた関数のこと
  • Ruby は全てがオブジェクトなので、メソッドと呼ぶのが正しい
  • とはいえ、メソッドも関数のことなので、関数と読んでも間違いというわけではない
  • 関数とメソッドの違いを聞かれてもちゃんと答えられるようにしておきたいねという話

参考 🔗

【SwitchBot】ボットとリモートボタンの電池

Tags: SwitchBot IoT

電池ボックスを開けて確認せず、Amazon で注文できるようにメモ

ボット 🔗

CR2

リモートボタン 🔗

CR2450

Mastodon のアカウントを作った

Tags: Mastodon

Twitter の雲行きが怪しくなり、とうとうサードパーティアプリも禁止になってしまった

いつでも移行できるように Mastodon のアカウントだけ作った

インスタンスは ruby.social を選んだ

https://ruby.social/@ytkg

Mastodon の仕組みはまだよくわかっていない

【Gatsby】ブログに SNS シェアボタンを設置した

Tags: Gatsby

今年はアクセス数を増やしていきたいと思っているので、 SNS シェアボタンを設置してみた

001.png

実装するにあたって、以下の記事を参考にした

コード 🔗

同じ Gatsby 製なのに書き方が違っていたので、一応参考として載せておく

import React from "react"
import {
  FacebookIcon,
  FacebookShareButton,
  HatenaIcon,
  HatenaShareButton,
  LineIcon,
  LineShareButton,
  TwitterIcon,
  TwitterShareButton,
  PocketIcon,
  PocketShareButton
} from "react-share"

const ShareButtonList = ({ title, url }) => {
  return (
    <>
      <h2>SNS でシェアする</h2>
      <div
        style={{
          display: `flex`,
          margin: `12px 0`
        }}
      >
        <div style={{ paddingRight: `12px` }}>
          <TwitterShareButton title={title} url={url} >
            <TwitterIcon size={60} round />
          </TwitterShareButton>
        </div>

        <div style={{ paddingRight: `12px` }}>
          <FacebookShareButton url={url}>
            <FacebookIcon size={60} round />
          </FacebookShareButton>
        </div>

        <div style={{ paddingRight: `12px` }}>
          <LineShareButton url={url} >
            <LineIcon size={60} round />
          </LineShareButton>
        </div>

        <div style={{ paddingRight: `12px` }}>
          <PocketShareButton url={url} >
            <PocketIcon size={60} round />
          </PocketShareButton>
        </div>

        <div style={{ paddingRight: `12px` }}>
          <HatenaShareButton url={url} >
            <HatenaIcon size={60} round />
          </HatenaShareButton>
        </div>
      </div>
    </>
  )
}

export default ShareButtonList

【Spotify Web API】アーティスト名を日本語で取得する

Tags: Spotify API

問題 🔗

Spotify Web API で現在再生中の曲を取得できるエンドポイントを叩いた時、アーティスト名が英語というかローマ字で返ってくる

$ curl -s -X GET https://api.spotify.com/v1/me/player/currently-playing \
  -H 'Authorization: Bearer {アクセストークン}' | jq -r '.item.artists[].name'
Ken Hirai
Aimyon

解決方法 🔗

ヘッダーに「Accept-Language: ja」を追加してあげれば日本語で取得することができる

$ curl -s -X GET https://api.spotify.com/v1/me/player/currently-playing \
  -H 'Accept-Language: ja' \
  -H 'Authorization: Bearer {アクセストークン}' | jq -r '.item.artists[].name'
平井堅
あいみょん

参考 🔗

Spotifyの検索APIで日本語の結果が返されないときの対処法 - Qiita

【Spotify Web API】現在再生中の曲を取得する

Tags: Spotify API

Spotify で現在再生中の曲を取得したかった

Spotify は API を公開しているので、Spotify Web API で取得することができる

$ curl -s -X GET https://api.spotify.com/v1/me/player/currently-playing \
-H 'Authorization: Bearer {アクセストークン}' | jq -r .item.name
君はロックを聴かない

アクセストークンを取得する方法 🔗

【Spotify Web API】アクセストークンの取得と更新(リフレッシュ) に書いた

API のレスポンス 🔗

$ curl -X GET https://api.spotify.com/v1/me/player/currently-playing \
-H 'Authorization: Bearer {アクセストークン}'
{
  "timestamp" : 1673277556270,
  "context" : {
    "external_urls" : {
      "spotify" : "https://open.spotify.com/playlist/37i9dQZF1DWZ7hCgzgU48z"
    },
    "href" : "https://api.spotify.com/v1/playlists/37i9dQZF1DWZ7hCgzgU48z",
    "type" : "playlist",
    "uri" : "spotify:playlist:37i9dQZF1DWZ7hCgzgU48z"
  },
  "progress_ms" : 5344,
  "item" : {
    "album" : {
      "album_type" : "album",
      "artists" : [ {
        "external_urls" : {
          "spotify" : "https://open.spotify.com/artist/5kVZa4lFUmAQlBogl1fkd6"
        },
        "href" : "https://api.spotify.com/v1/artists/5kVZa4lFUmAQlBogl1fkd6",
        "id" : "5kVZa4lFUmAQlBogl1fkd6",
        "name" : "Aimyon",
        "type" : "artist",
        "uri" : "spotify:artist:5kVZa4lFUmAQlBogl1fkd6"
      } ],
      "available_markets" : [ "AD", "AE", "AG", "AL", "AM", "AO", "AR", "AT", "AU", "AZ", "BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BN", "BO", "BR", "BS", "BT", "BW", "BY", "BZ", "CA", "CD", "CG", "CH", "CI", "CL", "CM", "CO", "CR", "CV", "CW", "CY", "CZ", "DE", "DJ", "DK", "DM", "DO", "DZ", "EC", "EE", "EG", "ES", "FI", "FJ", "FM", "FR", "GA", "GB", "GD", "GE", "GH", "GM", "GN", "GQ", "GR", "GT", "GW", "GY", "HK", "HN", "HR", "HT", "HU", "ID", "IE", "IL", "IN", "IQ", "IS", "IT", "JM", "JO", "JP", "KE", "KG", "KH", "KI", "KM", "KN", "KR", "KW", "KZ", "LA", "LB", "LC", "LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC", "MD", "ME", "MG", "MH", "MK", "ML", "MN", "MO", "MR", "MT", "MU", "MV", "MW", "MX", "MY", "MZ", "NA", "NE", "NG", "NI", "NL", "NO", "NP", "NR", "NZ", "OM", "PA", "PE", "PG", "PH", "PK", "PL", "PS", "PT", "PW", "PY", "QA", "RO", "RS", "RW", "SA", "SB", "SC", "SE", "SG", "SI", "SK", "SL", "SM", "SN", "SR", "ST", "SV", "SZ", "TD", "TG", "TH", "TJ", "TL", "TN", "TO", "TR", "TT", "TV", "TW", "TZ", "UA", "UG", "US", "UY", "UZ", "VC", "VE", "VN", "VU", "WS", "XK", "ZA", "ZM", "ZW" ],
      "external_urls" : {
        "spotify" : "https://open.spotify.com/album/0ct8ESCAYEpDGYJOndCfft"
      },
      "href" : "https://api.spotify.com/v1/albums/0ct8ESCAYEpDGYJOndCfft",
      "id" : "0ct8ESCAYEpDGYJOndCfft",
      "images" : [ {
        "height" : 640,
        "url" : "https://i.scdn.co/image/ab67616d0000b27333c05e1d08fec494a30bd240",
        "width" : 640
      }, {
        "height" : 300,
        "url" : "https://i.scdn.co/image/ab67616d00001e0233c05e1d08fec494a30bd240",
        "width" : 300
      }, {
        "height" : 64,
        "url" : "https://i.scdn.co/image/ab67616d0000485133c05e1d08fec494a30bd240",
        "width" : 64
      } ],
      "name" : "青春のエキサイトメント",
      "release_date" : "2017-09-12",
      "release_date_precision" : "day",
      "total_tracks" : 11,
      "type" : "album",
      "uri" : "spotify:album:0ct8ESCAYEpDGYJOndCfft"
    },
    "artists" : [ {
      "external_urls" : {
        "spotify" : "https://open.spotify.com/artist/5kVZa4lFUmAQlBogl1fkd6"
      },
      "href" : "https://api.spotify.com/v1/artists/5kVZa4lFUmAQlBogl1fkd6",
      "id" : "5kVZa4lFUmAQlBogl1fkd6",
      "name" : "Aimyon",
      "type" : "artist",
      "uri" : "spotify:artist:5kVZa4lFUmAQlBogl1fkd6"
    } ],
    "available_markets" : [ "AD", "AE", "AG", "AL", "AM", "AO", "AR", "AT", "AU", "AZ", "BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BN", "BO", "BR", "BS", "BT", "BW", "BY", "BZ", "CA", "CD", "CG", "CH", "CI", "CL", "CM", "CO", "CR", "CV", "CW", "CY", "CZ", "DE", "DJ", "DK", "DM", "DO", "DZ", "EC", "EE", "EG", "ES", "FI", "FJ", "FM", "FR", "GA", "GB", "GD", "GE", "GH", "GM", "GN", "GQ", "GR", "GT", "GW", "GY", "HK", "HN", "HR", "HT", "HU", "ID", "IE", "IL", "IN", "IQ", "IS", "IT", "JM", "JO", "JP", "KE", "KG", "KH", "KI", "KM", "KN", "KR", "KW", "KZ", "LA", "LB", "LC", "LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC", "MD", "ME", "MG", "MH", "MK", "ML", "MN", "MO", "MR", "MT", "MU", "MV", "MW", "MX", "MY", "MZ", "NA", "NE", "NG", "NI", "NL", "NO", "NP", "NR", "NZ", "OM", "PA", "PE", "PG", "PH", "PK", "PL", "PS", "PT", "PW", "PY", "QA", "RO", "RS", "RW", "SA", "SB", "SC", "SE", "SG", "SI", "SK", "SL", "SM", "SN", "SR", "ST", "SV", "SZ", "TD", "TG", "TH", "TJ", "TL", "TN", "TO", "TR", "TT", "TV", "TW", "TZ", "UA", "UG", "US", "UY", "UZ", "VC", "VE", "VN", "VU", "WS", "XK", "ZA", "ZM", "ZW" ],
    "disc_number" : 1,
    "duration_ms" : 246306,
    "explicit" : false,
    "external_ids" : {
      "isrc" : "JPWP01771557"
    },
    "external_urls" : {
      "spotify" : "https://open.spotify.com/track/59eluCMn0XbOWqeWQ91FTM"
    },
    "href" : "https://api.spotify.com/v1/tracks/59eluCMn0XbOWqeWQ91FTM",
    "id" : "59eluCMn0XbOWqeWQ91FTM",
    "is_local" : false,
    "name" : "君はロックを聴かない",
    "popularity" : 68,
    "preview_url" : "https://p.scdn.co/mp3-preview/7e26c8a2bd4b3fa7207d78baf796dd28994a8547?cid=1f2f34c40ba8411bbe57cdc7bd3f3632",
    "track_number" : 3,
    "type" : "track",
    "uri" : "spotify:track:59eluCMn0XbOWqeWQ91FTM"
  },
  "currently_playing_type" : "track",
  "actions" : {
    "disallows" : {
      "resuming" : true
    }
  },
  "is_playing" : true
}

【Spotify Web API】アクセストークンの取得と更新(リフレッシュ)

Tags: Spotify API

Spotify Web API を使うために必要なアクセストークンの取得方法とその更新(リフレッシュ)方法

認証フローが3つあり、今回の記事は「Authorization Code Flow」のやり方

事前準備 🔗

アプリを作成する 🔗

https://developer.spotify.com/dashboard/applications でアプリを作成し、Client ID と Client Secret を取得する

Redirect URL を設定する 🔗

アプリ詳細画面の「EDIT SETTINGS」から Redirect URL を設定する
ローカルで使うだけなのでなんでも良い
今回は「 http://localhost:3000 」を設定した

アクセストークンの取得 🔗

認証コードの取得する 🔗

以下の URL にブラウザでアクセスする

https://accounts.spotify.com/authorize?client_id={Client ID}&response_type=code&redirect_uri=http://localhost:3000&scope=user-read-currently-playing

client_id に事前準備で取得した Client ID を指定する
scope には許可したいものを指定する(今回の例では user-read-currently-playing)

アプリの認証画面が表示されるので「同意する」をクリック

すると以下のような URL にリダイレクトされるので、code の値をメモしておく(存在しない URL なので、エラーが表示されているが問題なし)

http://localhost:3000/?code=HOGEHOGE_CODE

Client ID と Client Secret を Base64 でエンコードする 🔗

$ echo -n {Client ID}:{Client Secret} | base64
BASE64_ENCODE_VALUE=

アクセストークンをリクエストする 🔗

$ curl -X POST https://accounts.spotify.com/api/token \
-H "Authorization: Basic {Client ID と Client Secret を Base64 でエンコードした値}" \
-d grant_type=authorization_code \
-d code={先ほどメモした code の値} \
-d redirect_uri=http://localhost:3000
{
  "access_token": "HOGEHOGE_ACCESS_TOKEN",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "HOGEHOGE_REFRESH_TOKEN",
  "scope": "user-read-currently-playing"
}

これでアクセストークンを取得することができた
リフレッシュトークンも必要な値なので、メモしておく

Cloudflare Workers と Hono でランダムに記事を表示させるやつを作った

Cloudflare Workers と Hono を使ってちゃんとした成果物を作ってないなと思ったので、ランダムに記事を表示させるやつを作った

これが “ちゃんとした成果物” に入るかどうかは疑問

成果物 🔗

https://random.takagi.blog

対象の記事はデータの入れ込みに手を抜いたので、現時点では2022/12/31までの記事になっている

仕組み 🔗

Cloudflare Workers と Hono に加えて、記事データの保存先として Cloudflare D1 も使っている

アクセスが来たらデータベースからランダムに記事を1つ取り出してリダイレクトさせているだけ

import { Hono } from 'hono'

interface Env {
  DB: D1Database
}

type Article = {
  id: number
  title: string
  link: string
}

const app = new Hono<{ Bindings: Env }>()

app.get('/', async (c) => {
  const article = await c.env.DB.prepare(
    `select * from articles order by random()`
  ).first<Article>()

  return c.redirect(article.link)
})

export default app

ランダムに記事を1つ取り出すのに、order by random() という SQL アンチパターンを使ってしまっているが、このくらいの用途では問題ないだろうと判断した

記事データは書き慣れている Ruby でサクッと INSERT 文を生成して D1 にぶっ込んだ

require 'rss'

rss = RSS::Parser.parse('https://takagi.blog/rss.xml')
rss.items.each.with_index(1) do |item, i|
  puts "INSERT INTO articles VALUES (#{i}, '#{item.title}', '#{item.link}');"
end
$ ruby app.rb > data.sql

リポジトリ 🔗

https://github.com/ytkg/random-takagi-blog

2022年を振り返る

Tags: 振り返り

2022年を振り返る

ブログ 🔗

昨年に引き続き、4日ごと記事を書くようにしていて、計90記事書いた(この記事を含めると91記事)

Cloudflare Advent Calendar 2022 で書いた記事がはてブに載った(ブログの画像配信サーバーとして Cloudflare R2 を使う

短期的なアウトプット(ブログなど)のための学習をやめた - チョキチョキかにさん 」を読んで、自分もブログネタのためにそのような節があったなと気がついたので来年は頻度を減らすかもしれない

OSS 貢献 🔗

このブログでは公言していないが、月1ペースで OSS に貢献するという目標を立てていた

実績はプルリクエストだけでも21個マージされたので、余裕で達成

他にも Isuue での貢献もいくつかある

草生やし活動(GitHub コントリビューション) 🔗

1706 contributions だった

001.png

後半くらいから、同僚と毎日草を生やそう!とお互いに圧を掛ける活動が始まったので草が生えまくってる

Dependabot が作成したプルリクエストをマージするだけの日もあるが、一旦 OK としている

継続するためにはハードルを下げることが大事だと思っているので、これからもしばらくはそのルールにするつもり

連続継続日数、365日を目指したい

「2022年やりたいこと」の達成状況 🔗

覚えていなかったが、以下7個のやりたいことを挙げていた

  • Deno モジュールを公開する
  • takagi.dev でツールを公開する
  • Cloud Functions を触る
  • Cloud Run を触る
  • PlanetScale を触る
  • おうち Kubenetes(おうちクラウド)をやる
  • Rust を触る

せっかくなので振り返ってみる

Deno モジュールを公開する 🔗

達成

ブログにはまだ書いていないが個人サービスを作っていて、その CLI ツールを Deno で作って公開した

Deno モジュールは GitHub リポジトリと連携させてタグを打つだけでリリースされる仕組みなので、tagpr を導入してプルリクエストをマージするだけでリリースできるようにした

リリース用のpull requestを自動作成し、マージされたら自動でタグを打つtagpr | おそらくはそれさえも平凡な日々

takagi.dev でツールを公開する 🔗

未達成

GitHub Actions の ruby/setup-ruby で Ruby のバージョンを .ruby-version から取得して指定する

以下の記事を読んで Ruby もできたらいいなと調べたら、書き方は違うができる方法がちゃんと用意されていた

GitHub Actions の setup-go や setup-node で指定されるバージョンを go.mod や .node-version から取ってくる - stefafafan の fa は3つです

これで Rails アプリとかの Ruby バージョンを上げる際に変更するファイルが1つ減らせる

やり方 🔗

with で ruby-version に .ruby-version と指定するだけ

      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: .ruby-version

default にしてもよい(.ruby-version、.tool-versions の順で読み込む仕様)

      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: default

もっというと、デフォルト値が default になっているので何も指定しなくても良かったりする
でもちゃんと書いた方が、.ruby-version から読み込んでいることが一目でわかるので親切だと思う

実装について 🔗

ついでにどうやって実現しているのか気になったからコードを読んだ
説明不要なシンプルな実装になってた

  if (rubyVersion === 'default') {
    if (fs.existsSync('.ruby-version')) {
      rubyVersion = '.ruby-version'
    } else if (fs.existsSync('.tool-versions')) {
      rubyVersion = '.tool-versions'
    } else {
      throw new Error('input ruby-version needs to be specified if no .ruby-version or .tool-versions file exists')
    }
  }

  if (rubyVersion === '.ruby-version') { // Read from .ruby-version
    rubyVersion = fs.readFileSync('.ruby-version', 'utf8').trim()
    console.log(`Using ${rubyVersion} as input from file .ruby-version`)
  } else if (rubyVersion === '.tool-versions') { // Read from .tool-versions
    const toolVersions = fs.readFileSync('.tool-versions', 'utf8').trim()
    const rubyLine = toolVersions.split(/\r?\n/).filter(e => /^ruby\s/.test(e))[0]
    rubyVersion = rubyLine.match(/^ruby\s+(.+)$/)[1]
    console.log(`Using ${rubyVersion} as input from file .tool-versions`)
  }

https://github.com/ruby/setup-ruby/blob/8a6667e214803d030def654bc64a71ceeec500d1/index.js

ブログの画像配信サーバーとして Cloudflare R2 を使う

この記事は Cloudflare Advent Calendar 2022 の23日目の記事です

今までブログの画像配信サーバーとして Google Cloud Storage を使っていたが、ブログを Cloudflare Pages に移行した こともあり、どうせならと画像のホスティングも Cloudflare R2 に移行することにした

R2 が GA (一般公開) され、パブリックアクセス(独自ドメイン可)ができるようになったのも理由の一つにある(今までは Cloudflare Workers 経由でしかパブリックアクセスできなかった)

Cloudflare R2 とは 🔗

簡単にいうと、エグレス料金の掛からない Amazon S3 互換のオブジェクトストレージ

料金体系 🔗

  • ストレージ: 月額 $0.015 / GB
  • クラス A 操作: 月額 $4.50 / 100万回
  • クラス B 操作: 月額 $0.36 / 100万回

クラス A 操作は「状態を変更(書き込み)」、クラス B 操作は「既存の状態の読み込み」のこと

無料枠もある 🔗

  • ストレージ: 10 GB / 月
  • クラス A 操作: 100万回 / 月
  • クラス B 操作: 1000万回 / 月

個人利用レベルであれば、ほぼ無料で使えるはず

Next.js 13 の app ディレクトリに対して ESLint を実行する

Tags: Next.js ESLint

yarn lint (next lint) を実行しても、Next.js 13 からできた app ディレクトリに対しては実行されなかった

どうやらデフォルトでは、pagescomponentslibsrc ディレクトリ以下にしか実行されないらしい

export const ESLINT_DEFAULT_DIRS = ['pages', 'components', 'lib', 'src']

https://github.com/vercel/next.js/blob/04daf7eb0687c4cc891b5efb720280fbd7fb8f04/packages/next/lib/constants.ts#L42

じゃあどうやるか

Next.js の設定ファイル(next.config.js)に対象にしたいディレクトリを設定してあげたら良い

module.exports = {
  eslint: {
    dirs: ['app', 'pages']
  },
}

https://nextjs.org/docs/basic-features/eslint#linting-custom-directories-and-files

参考 🔗

Next ESLint does not match ESLint (App directory) · Issue #43021 · vercel/next.js

【Mac】複数ファイルの文字列を一括で置換する

OS によってはコマンドの挙動が微妙に違うので、これは Mac の場合のやり方

やり方 🔗

$ grep -lr '置換前の文字列' . | xargs sed -i '' 's/置換前の文字列/置換後の文字列/g'

実例 🔗

以下コマンドで、この PR の修正をした

$ grep -lr ':type => ' . | xargs sed -i '' 's/:type => /type: /g'

【Go】2006/01/02 15:04:05

Tags: Go

yyyy/mm/dd などで表す日付/時刻のフォーマットを、Go 言語では「2006/01/02 15:04:05」で表す

package main

import (
    "fmt"
    "time"
)

func main() {
    now := time.Now()
    fmt.Println(now)
    fmt.Println(now.Format("2006/01/02 15:04:05"))
}
$ go run main.go
2022-12-11 18:55:52.37557 +0900 JST m=+0.000096324
2022/12/11 18:55:52

時間の意味 🔗

Go 言語が生まれた日とかではなく、アメリカ方式の時刻の順番で 1, 2, 3, 4, 5, 6 になっている

「1月2日午後3時4分5秒2006年」

参考 🔗

Goのtimeパッケージのリファレンスタイム(2006年1月2日)は何の日? - Qiita

Raspberry Pi の冷却ファンを交換した

Tags: Raspberry Pi

Raspberry Pi で使っていた、冷却ファンから異音がするようになったから交換した

24時間365日ずっと動いてたら、そりゃ調子悪くなる

Amazon で元から付いてたファンと同じブランドっぽいものを見つけたので購入
GeeekPi Raspberry Pi 4 冷却ファン

大きさは一緒、30mm * 30mm * 7mm

型番が微妙に違った

  • 元から付いてたもの: GF3007S
  • 今回買ったもの: GF3007BS

付属していたネジの頭が大きくて、無理やりケースの蓋を閉めたけど問題はなさそう(元のネジは使い回せなかった)

Categories


Tags