Mac に Homebrew で 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 を上書きしている
重複したコマンドは履歴に残さない設定 もするとより良い
参考 🔗
【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】重複したコマンドは履歴に残さない設定
重複したコマンドは履歴に残さないようにしたい
手順 🔗
.bashrc とかに以下の設定を追記すればいい
HISTCONTROL=erasedups
設定を反映
$ source .bashrc
補足 🔗
挙動 🔗
履歴にコマンドを保存する時に同じコマンドの履歴をすべて削除するという挙動になっているので、設定をしたからといってすぐに重複カットされるわけではない
HISTCONTROL で設定できる項目 🔗
- ignorespace: 空白文字で始まる行を保存しない
- ignoredups: ひとつ前の履歴エントリと一致する行を保存しない
- ignoreboth: ignorespace と ignoredups の省略形
- erasedups: 現在の行と一致する履歴を保存前にすべて削除する
参考 🔗
【Hono】create-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 からバリデーションの書き方が変わった
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 がすごい
今更だけど、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】関数とメソッドの違い
関数とメソッドの違いを聞かれて「同じでしょ」と答えてしまったけど、厳密には違いがあったのでここにメモしておく
関数とメソッドの違い 🔗
関数 🔗
何かしらの処理をひとまとめにしたもの
引数を受け取ったり、戻り値を返したりもする(もちろんなくても良い)
メソッド 🔗
オブジェクトに結びつけられた関数
Ruby における関数 🔗
全てがオブジェクトとなる Ruby では、メソッドと呼ぶのが正しい
puts
は組み込み関数と思いきや、 Kernel
モジュールに定義されているのでメソッドである
結論 🔗
- Ruby (オブジェクト指向言語)において、関数とメソッドは厳密には違う
- メソッドはオブジェクトに結びつけられた関数のこと
- Ruby は全てがオブジェクトなので、メソッドと呼ぶのが正しい
- とはいえ、メソッドも関数のことなので、関数と読んでも間違いというわけではない
- 関数とメソッドの違いを聞かれてもちゃんと答えられるようにしておきたいねという話
参考 🔗
【SwitchBot】ボットとリモートボタンの電池
Mastodon のアカウントを作った
Twitter の雲行きが怪しくなり、とうとうサードパーティアプリも禁止になってしまった
いつでも移行できるように Mastodon のアカウントだけ作った
インスタンスは ruby.social を選んだ
Mastodon の仕組みはまだよくわかっていない
【Gatsby】ブログに SNS シェアボタンを設置した
今年はアクセス数を増やしていきたいと思っているので、 SNS シェアボタンを設置してみた
実装するにあたって、以下の記事を参考にした
- GatsbyサイトにSNSシェアボタンを設置する - Qiita
- Gatsbyブログにreact-shareでSNSシェアボタンを設置する | ネコニキの開発雑記
- Gatsby(GatsbyJS)でSNSのSHAREを設置 | Gatsbyブログカスタマイズ | フロントセンセイ
コード 🔗
同じ 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】アーティスト名を日本語で取得する
問題 🔗
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 Web 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】アクセストークンの取得と更新(リフレッシュ)
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 を使ってちゃんとした成果物を作ってないなと思ったので、ランダムに記事を表示させるやつを作った
これが “ちゃんとした成果物” に入るかどうかは疑問
成果物 🔗
対象の記事はデータの入れ込みに手を抜いたので、現時点では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
リポジトリ 🔗
2022年を振り返る
2022年を振り返る
ブログ 🔗
昨年に引き続き、4日ごと記事を書くようにしていて、計90記事書いた(この記事を含めると91記事)
Cloudflare Advent Calendar 2022 で書いた記事がはてブに載った(ブログの画像配信サーバーとして Cloudflare R2 を使う )
「短期的なアウトプット(ブログなど)のための学習をやめた - チョキチョキかにさん 」を読んで、自分もブログネタのためにそのような節があったなと気がついたので来年は頻度を減らすかもしれない
OSS 貢献 🔗
このブログでは公言していないが、月1ペースで OSS に貢献するという目標を立てていた
実績はプルリクエストだけでも21個マージされたので、余裕で達成
他にも Isuue での貢献もいくつかある
草生やし活動(GitHub コントリビューション) 🔗
1706 contributions だった
後半くらいから、同僚と毎日草を生やそう!とお互いに圧を掛ける活動が始まったので草が生えまくってる
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 もできたらいいなと調べたら、書き方は違うができる方法がちゃんと用意されていた
これで 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 を実行する
yarn lint (next lint) を実行しても、Next.js 13 からできた app ディレクトリに対しては実行されなかった
どうやらデフォルトでは、pages、components、lib、src ディレクトリ以下にしか実行されないらしい
export const ESLINT_DEFAULT_DIRS = ['pages', 'components', 'lib', 'src']
じゃあどうやるか
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】複数ファイルの文字列を一括で置換する
【Go】2006/01/02 15:04:05
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年」
参考 🔗
Raspberry Pi の冷却ファンを交換した
Raspberry Pi で使っていた、冷却ファンから異音がするようになったから交換した
24時間365日ずっと動いてたら、そりゃ調子悪くなる
Amazon で元から付いてたファンと同じブランドっぽいものを見つけたので購入
GeeekPi Raspberry Pi 4 冷却ファン
大きさは一緒、30mm * 30mm * 7mm
型番が微妙に違った
- 元から付いてたもの: GF3007S
- 今回買ったもの: GF3007BS
付属していたネジの頭が大きくて、無理やりケースの蓋を閉めたけど問題はなさそう(元のネジは使い回せなかった)