NotionRubyMapping への AudioBlock の実装 : hkob の雑記録 (350)

はじめに

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 内に以下のようなブロックを置きました。

image.png

このブロックを現在の 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 の塊なので、ほぼ辞書や定数を設定するだけになっているだけです。初見の人はテストを見ても何をやっているかわからないかもしれませんね。

hkob.notion.site