はじめに
hkob の雑記録の第350回目は、NotionRubyMapping に AudioBlock を追加します。本記事は2025年ユカスタマスの16日目(2023年ユカスタマスの747日目)の記事になります。
実行前の確認
とりあえず作業を始める前にテストが失敗していないことを確認します。
Finished in 1.1 seconds (files took 0.93535 seconds to load) 2951 examples, 0 failures
audio_block_spec.rb の作成
基本的に audio は video_block_spec.rb と内容が一緒なので、ひとまずコピーします。
cp spec/notion_ruby_mapping/blocks/{video,audio}_block_spec.rb
テスト用に Block test page 内に以下のようなブロックを置きました。

このブロックを現在の NotionRubyMapping に読み込んでみます。AudioBlock を作成していないので、親の Block になっています。
block = Block.find "https://www.notion.so/hkob/Block-test-page-67cf059ce74646a0b72d481c9ff5d386?source=copy_link#2cad8e4e98ab80efb1fdca3b581b4c2c" => NotionRubyMapping::Block-2cad8e4e98ab80efb1fdca3b581b4c2c
JSON を覗いてみると以下のようになっていました。
print JSON.pretty_generate(block.json) { "object": "block", "id": "2cad8e4e-98ab-80ef-b1fd-ca3b581b4c2c", "parent": { "type": "page_id", "page_id": "67cf059c-e746-46a0-b72d-481c9ff5d386" }, "created_time": "2025-12-15T11:18:00.000Z", "last_edited_time": "2025-12-15T11:19:00.000Z", "created_by": { "object": "user", "id": "2200a911-6a96-44bb-bd38-6bfb1e01b9f6" }, "last_edited_by": { "object": "user", "id": "2200a911-6a96-44bb-bd38-6bfb1e01b9f6" }, "has_children": false, "archived": false, "in_trash": false, "type": "audio", "audio": { "caption": [], "type": "external", "external": { "url": "https://download.samplelib.com/mp3/sample-3s.mp3" } }, "request_id": "c1393f1c-be85-41d4-adad-69874e66348d" }=> nil
これに合わせて audio_block_spec.rb の上部を以下のように変更しました。
# frozen_string_literal: true require_relative "../../spec_helper" module NotionRubyMapping RSpec.describe AudioBlock do type = "audio" it_behaves_like "retrieve block", described_class, TestConnection.block_id(type), false, { "object" => "block", "type" => "audio", "audio" => { "caption" => [], "type" => "external", "external" => { "url" => "https://download.samplelib.com/mp3/sample-3s.mp3", }, }, }
type から block_id が取得できるように BLOCK_ID_HASH に上で作成したブロックの ID を登録しておきます。
BLOCK_ID_HASH = { audio: "2cad8e4e98ab80efb1fdca3b581b4c2c", (中略) video: "bed3abe020094aa990564844f981b07a", }.freeze
このファイルを保存すると retrieve_block_audio.json が読み込めないというエラーになります。ここに block_id を登録すると、自動的に retrieve した結果の JSON を読み込むようにしていたためでした。このため、retrieve_block_audio.sh を作成し、このファイルを作成します。まずは video のものをコピーします。
cp retrieve_block_{video,audio}.sh
これも先ほどの ID に書き換えるだけです。
curl 'https://api.notion.com/v1/blocks/2cad8e4e98ab80efb1fdca3b581b4c2c' \
-H 'Authorization: Bearer '"$NOTION_API_KEY"'' \
-H 'Notion-Version: 2025-09-03'
make すると retrieve_block_audio.json が作成されます。
touch *.json; make
sh retrieve_block_audio.sh > retrieve_block_audio.json
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 614 100 614 0 0 1156 0 --:--:-- --:--:-- --:--:-- 1158
sleep 1
ここまで準備するとテストが正しく起動しますが、当然以下のようなエラーになります。
NameError: uninitialized constant NotionRubyMapping::AudioBlock
audio_block.rb の作成
audio_block.rb が必要なので、コピーします。
cp lib/notion_ruby_mapping/blocks/{video,audio}_block.rb
以下のように修正します。基本的な機能は全て FileBaseBlock に記載されているので、クラス名と type の文字列を変更するだけです。
# frozen_string_literal: true module NotionRubyMapping # Notion block class AudioBlock < FileBaseBlock # @return [String] def type "audio" end end end
あとは notion_ruby_mapping で audio_block を require するようにしました。ここまですると、以下のようなエラーになりました。これは audio という type に対して AudioBlock を結びつけていないためです。
Failures:
1) NotionRubyMapping::AudioBlock behaves like retrieve block NotionRubyMapping::AudioBlock block behaves like raw json is expected to eq {"audio" => {"caption" => [], "external" => {url: "https://download.samplelib.com/mp3/sample-3s.mp3"}, "type" => "external"}, "object" => "block", "type" => "audio"}
Failure/Error: expect(target.send(key)).to eq json
expected: {"audio" => {"caption" => [], "external" => {url: "https://download.samplelib.com/mp3/sample-3s.mp3"}, "type" => "external"}, "object" => "block", "type" => "audio"}
got: {"object" => "block", "type" => nil}
(compared using ==)
Diff:
@@ -1,3 +1,2 @@
-"audio" => {"caption" => [], "external" => {url: "https://download.samplelib.com/mp3/sample-3s.mp3"}, "type" => "external"},
"object" => "block",
-"type" => "audio",
+"type" => nil,
Shared Example Group: "raw json" called from ./spec/spec_helper.rb:3126
Shared Example Group: "retrieve block" called from ./spec/notion_ruby_mapping/blocks/audio_block_spec.rb:9
# ./spec/spec_helper.rb:2940:in 'block (2 levels) in <module:RSpec>'
これは block.rb の type2class によるものです。ここに audio を追加しました。
def self.type2class(type, has_children = false) @type2class ||= { false => { audio: AudioBlock, (中略) video: VideoBlock, }, true => { heading_1: ToggleHeading1Block, (中略) }, } @klass = @type2class[has_children][type.to_sym] || @type2class[false][type.to_sym] || Block end
これで、retrieve のテストは通過しました。
NotionRubyMapping::AudioBlock behaves like retrieve block NotionRubyMapping::AudioBlock block receive id can NotionRubyMapping::AudioBlock have children? behaves like raw json is expected to eq {"audio" => {"caption" => [], "external" => {"url" => "https://download.samplelib.com/mp3/sample-3s.mp3"}, "type" => "external"}, "object" => "block", "type" => "audio"} Finished in 0.0701 seconds (files took 0.35724 seconds to load) 3 examples, 0 failures
append_block_children のテストを追加
実装はほぼ何も記述していないので、問題なく動作するとは思いますが、念のため作成や更新もテストで確認しておきます。まずは append_block_children_page_audio.sh と append_block_children_block_audio.sh を作成しておきます。
cp append_block_children_block_{video,audio}.sh
cp append_block_children_page_{video,audio}.sh
append_block_children_block_audio.sh は以下のようになります。
#!/bin/sh curl -X PATCH 'https://api.notion.com/v1/blocks/26cd8e4e98ab8035a5b4ea240d930619/children' \ -H 'Notion-Version: 2025-09-03' \ -H 'Authorization: Bearer '"$NOTION_API_KEY"'' \ -H 'Content-Type: application/json' \ --data '{"children":[{"type":"audio","object":"block","audio":{"type":"external","external":{"url":"https://download.samplelib.com/mp3/sample-3s.mp3"}}}]}'
同様に append_block_children_page_audio.sh は以下のようになります。
#!/bin/sh curl -X PATCH 'https://api.notion.com/v1/blocks/26cd8e4e98ab8061b880f8f45db00383/children' \ -H 'Notion-Version: 2025-09-03' \ -H 'Authorization: Bearer '"$NOTION_API_KEY"'' \ -H 'Content-Type: application/json' \ --data '{"children":[{"type":"audio","object":"block","audio":{"type":"external","external":{"url":"https://download.samplelib.com/mp3/sample-3s.mp3"}}}]}'
make しました。
touch *.json; make sh append_block_children_block_audio.sh > append_block_children_block_audio.json % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 854 100 708 100 146 445 91 0:00:01 0:00:01 --:--:-- 537 sleep 1 sh append_block_children_page_audio.sh > append_block_children_page_audio.json % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 852 100 706 100 146 337 69 0:00:02 0:00:02 --:--:-- 407 sleep 1
取得した JSON の id が返却されることをテストで確認します。
describe "create_child_block" do let(:target) do described_class.new "https://download.samplelib.com/mp3/sample-3s.mp3" end it_behaves_like "create child block", described_class, "2cad8e4e98ab8161a64bc8d7e9f6fc5d", "2cad8e4e98ab8195b8dee4a5569ad95a" end
また、webmock の stub が設定されていないので、エラーになります。上記の JSON を webmock に読み込ませるには、append_block_children_hash に payload を登録すればいいようにしてあります。以下のように audio の payload を登録しました。
def append_block_children_hash(id) { audio: [ id, 200, { "children" => [ { "type" => "audio", "object" => "block", "audio" => { "type" => "external", "external" => { "url" => "https://download.samplelib.com/mp3/sample-3s.mp3" } } } ] } ], (後略)
これで、create_block のテストが無事に通過しました。実際にブロック追加のテストができるだけでなく、dry_run で作成される shell script が正しく生成されているかもテストされています。
NotionRubyMapping::AudioBlock behaves like retrieve block NotionRubyMapping::AudioBlock block receive id can NotionRubyMapping::AudioBlock have children? behaves like raw json is expected to eq {"audio" => {"caption" => [], "external" => {"url" => "https://download.samplelib.com/mp3/sample-3s.mp3"}, "type" => "external"}, "object" => "block", "type" => "audio"} create_child_block behaves like create child block create child block when for page when dry_run behaves like dry run is expected to eq "#!/bin/sh\ncurl -X PATCH 'https://api.notion.com/v1/blocks/26cd8e4e98ab8061b880f8f45db00383/children...l\",\"external\":{\"url\":\"https://download.samplelib.com/mp3/sample-3s.mp3\"},\"caption\":[]}}]}'" when create is expected to eq "2cad8e4e98ab8161a64bc8d7e9f6fc5d" when for block when dry_run behaves like dry run is expected to eq "#!/bin/sh\ncurl -X PATCH 'https://api.notion.com/v1/blocks/26cd8e4e98ab8035a5b4ea240d930619/children...l\",\"external\":{\"url\":\"https://download.samplelib.com/mp3/sample-3s.mp3\"},\"caption\":[]}}]}'" when create is expected to eq "2cad8e4e98ab8195b8dee4a5569ad95a"
update_block のテスト
update_block も video からコピーします。まず、caption の更新です。
cp update_block_{video,audio}_caption.sh
スクリプトは以下のようになります。
#!/bin/sh curl -X PATCH 'https://api.notion.com/v1/blocks/2cad8e4e98ab806292f9cb56918b2c4c' \ -H 'Notion-Version: 2025-09-03' \ -H 'Authorization: Bearer '"$NOTION_API_KEY"'' \ -H 'Content-Type: application/json' \ --data '{"audio":{"caption":[{"type":"text","text":{"content":"New caption","link":null},"plain_text":"New caption","href":null}]}}'
make します。
touch *.json; make sh update_block_audio_caption.sh > update_block_audio_caption.json % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 953 100 830 100 123 114 17 0:00:07 0:00:07 --:--:-- 193 sleep 1
同様に URL の変更スクリプトも用意します。
#!/bin/sh curl -X PATCH 'https://api.notion.com/v1/blocks/2cad8e4e98ab806292f9cb56918b2c4c' \ -H 'Notion-Version: 2025-09-03' \ -H 'Authorization: Bearer '"$NOTION_API_KEY"'' \ -H 'Content-Type: application/json' \ --data '{"audio":{"external":{"url":"https://download.samplelib.com/mp3/sample-6s.mp3"}}}'
こちらも make します。
touch *.json; make sh update_block_audio_url.sh > update_block_audio_url.json % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 911 100 830 100 81 286 27 0:00:03 0:00:02 0:00:01 314 sleep 1
update 部分のテストは以下のようになります。
describe "save (update)" do let(:update_id) { TestConnection.update_block_id(type) } let(:target) do described_class.new "https://download.samplelib.com/mp3/sample-3s.mp3", caption: "Old caption", id: update_id end it_behaves_like "update block caption", type, "New caption" it_behaves_like "update block file", type, "https://download.samplelib.com/mp3/sample-6s.mp3" end
update_block_id を取得するために、UPDATE_BLOCK_ID_HASH の audio にアップデートする block の ID を登録しておきます。
UPDATE_BLOCK_ID_HASH = { audio: "2cad8e4e98ab806292f9cb56918b2c4c",
あとは、update_block の webmock の設定も追加します。こちらも payload と JSON ファイルの関連を指定するだけです。
def update_block generate_stubs_sub :patch, __method__, :block_path, { audio_caption: [ UPDATE_BLOCK_ID_HASH[:audio], 200, { "audio" => { "caption" => [ { "type" => "text", "text" => { "content" => "New caption", "link" => nil, }, "plain_text" => "New caption", "href" => nil, }, ], }, } ], audio_url: [ UPDATE_BLOCK_ID_HASH[:audio], 200, { "audio" => { "external" => { "url" => "https://download.samplelib.com/mp3/sample-6s.mp3", }, }, } ],
結果として、全てのテストが通過しました。
NotionRubyMapping::AudioBlock behaves like retrieve block NotionRubyMapping::AudioBlock block receive id can NotionRubyMapping::AudioBlock have children? behaves like raw json is expected to eq {"audio" => {"caption" => [], "external" => {"url" => "https://download.samplelib.com/mp3/sample-3s.mp3"}, "type" => "external"}, "object" => "block", "type" => "audio"} create_child_block behaves like create child block create child block when for page when dry_run behaves like dry run is expected to eq "#!/bin/sh\ncurl -X PATCH 'https://api.notion.com/v1/blocks/26cd8e4e98ab8061b880f8f45db00383/children...l\",\"external\":{\"url\":\"https://download.samplelib.com/mp3/sample-3s.mp3\"},\"caption\":[]}}]}'" when create is expected to eq "2cad8e4e98ab8161a64bc8d7e9f6fc5d" when for block when dry_run behaves like dry run is expected to eq "#!/bin/sh\ncurl -X PATCH 'https://api.notion.com/v1/blocks/26cd8e4e98ab8035a5b4ea240d930619/children...l\",\"external\":{\"url\":\"https://download.samplelib.com/mp3/sample-3s.mp3\"},\"caption\":[]}}]}'" when create is expected to eq "2cad8e4e98ab8195b8dee4a5569ad95a" save (update) behaves like update block caption is expected to eq {"audio" => {"caption" => [{"href" => nil, "plain_text" => "New caption", "text" => {"content" => "New caption", "link" => nil}, "type" => "text"}]}} when dry_run behaves like dry run is expected to eq "#!/bin/sh\ncurl -X PATCH 'https://api.notion.com/v1/blocks/2cad8e4e98ab806292f9cb56918b2c4c' \\\n -...ext\":{\"content\":\"New caption\",\"link\":null},\"plain_text\":\"New caption\",\"href\":null}]}}'" when save is expected to eq "New caption" behaves like update block file is expected to eq {"audio" => {"external" => {"url" => "https://download.samplelib.com/mp3/sample-6s.mp3"}}} when dry_run behaves like dry run is expected to eq "#!/bin/sh\ncurl -X PATCH 'https://api.notion.com/v1/blocks/2cad8e4e98ab806292f9cb56918b2c4c' \\\n -... --data '{\"audio\":{\"external\":{\"url\":\"https://download.samplelib.com/mp3/sample-6s.mp3\"}}}'" when save is expected to eq "https://download.samplelib.com/mp3/sample-6s.mp3" Finished in 0.06256 seconds (files took 0.2939 seconds to load) 13 examples, 0 failures
version 変更と 3.0.5 のリリース
最後に version.rb の内容を更新します。
# frozen_string_literal: true module NotionRubyMapping VERSION = "3.0.5" NOTION_VERSION = "2025-09-03" end
ここまでの修正を commit し、プルリクエストを作成します。main を更新して build してみます。
rake build notion_ruby_mapping 3.0.5 built to pkg/notion_ruby_mapping-3.0.5.gem.
正しく生成されているようです。うまく動いているようなので、release します。
rake release notion_ruby_mapping 3.0.5 built to pkg/notion_ruby_mapping-3.0.5.gem. Tagged v3.0.5. Pushed git commits and release tag. Pushing gem to https://rubygems.org... You have enabled multi-factor authentication. Please enter OTP code. Code: ****** Successfully registered gem: notion_ruby_mapping (3.0.5) Pushed notion_ruby_mapping 3.0.5 to rubygems.org
おわりに
実装はたった2行の修正だけなのですが、そのためのテストデータ作成はすごい量ですね。実際のテスト自体も shared_example の塊なので、ほぼ辞書や定数を設定するだけになっているだけです。初見の人はテストを見ても何をやっているかわからないかもしれませんね。