📚

Kobo APIで続刊通知を作る:その2

2024/06/15に公開

その2

APIを呼ぶ準備ができたので実際に呼び出す部分を作っていきます。

とはいえ、まだいくつか周辺関数を作ったほうがよいのでそちらを先に。

各種変数と初期化

Kobo API は UTF8 エンコーディングで検索文字を渡す必要があるため、その Encoding をあらかじめ変数化しておきます。

また、探したい最新刊とは別のものが検索結果として返ってくるので、それを除外するために NG ワードを設定します。具体的には、まとめた1巻単位を買いたいので、分冊、話売り、単話、連載版を指定し、「ばら売り」のものを除外します。またキャンペーンでのセット売り、1巻無料などがあるので、それも除外します。

初期化関数は application Id をもらって覚えておくほかに、ジャンルデータの読み込みもしておきましょう。

KoboAPI.psm1
    [string]$AppId;
    $Encoding = [System.Text.Encoding]::GetEncoding('UTF-8');
    [string]$NGWord = "分冊 話売り 単話版 連載版 巻セット 限定無料";
    [string]$NGWordParam = "NGKeywords=" + [System.Web.HttpUtility]::UrlEncode($this.NGWord, $this.Encoding);

    Init([string]$id) {
        $this.AppId = $id

        $this.LoadGenre()
    }

検索

ようやく検索まで来ました。ここまでくれば後は、タイトルとジャンル、追加キーワードを受け取って API 仕様に沿って URL で送るだけです。

検索結果を簡素化するために formatVersion=2 を指定、結果としてタイトル、シリーズ名、著作者、Url、ジャンル ID を受け取るようにしておきましょう。

API から返されたデータのうち、指定したタイトルが最初にあるものを選び、日付が最新のものを結果とします。

検索結果がない時に適当にメッセージを出しておきます(後で消す)。

KoboAPI.psm1

    [Object] SearchKobo([string]$title, [string]$genre, [string]$keywords) {
        $url = @("https://app.rakuten.co.jp/services/api/Kobo/EbookSearch/20170426?applicationId=$($this.AppId)")
        $url += ,"title=$([System.Web.HttpUtility]::UrlEncode($title, $this.Encoding))"
        $url += ,$this.GetGenreIdParam($genre)
        if ($keywords) {
            $url += ,"keyword=$([System.Web.HttpUtility]::UrlEncode($keywords, $this.Encoding))"
        }
        $url += ,"formatVersion=2"
        $url += ,"elements=title,seriesName,author,salesDate,itemUrl,koboGenreId"
        $url += ,"field=1"

        $res = $this.APIGet($url -join('&'))
        if ($res.Contains('Items')) {
            $r = $res.Items |Sort-Object @{Expression={$_.salesDate};Descending=1} |Where-Object { $_.title -match "^$title" }
            if ($r) {
                return $r |Select-Object -First 1
            } else {
                write-host "$($res.Items.Count)件が検索にヒットしましたシリーズタイトルが一致するものはありませんでした"
                $res.Items |%{ write-host "$($_.title) [$($t_.seriesName)]"}
                return $null
            }
        } else {
            Write-Host "検索結果なし: $title in $genre, $keywords"
            return $null
        }
    }


テスト

そろそろインスタンス生成から毎回入力していくのもアレなので、この関数を呼び出す側もテスト用に作ります。

test.ps1
using module .\KoboAPI.psm1

using module .\KoboAPI.psm1

param(
[string]$Title,
[string]$Genre,
[string]$Keywords
)

$appId = "<ID>"

$api = [KoboAPI]::New()
$api.Init($appId)

$api.SearchKobo($script:Title, $script:Genre, $script:Keywords)

タイトル、ジャンル、キーワードを受け取って検索用関数(SearchKobo)を呼び出すだけです。


> pwsh test.ps1 ミステリと言う

Name                           Value
----                           -----
author                         田村由美
itemUrl                        https://books.rakuten.co.jp/rk/5a69eba0dd65359088631f2e2a60f1b0/
koboGenreId                    101904012007
salesDate                      2024年06月10日
seriesName                     ミステリと言う勿れ
title                          ミステリと言う勿れ(14)


> pwsh test.ps1 転生したらスライム コミック

Name                           Value
----                           -----
author                         川上泰樹/伏瀬/みっつばー
itemUrl                        https://books.rakuten.co.jp/rk/df9dfbd0a6cf3e2bb803d2871a3e4d0a/
koboGenreId                    101904002048
salesDate                      2024年06月07日
seriesName                     転生したらスライムだった件
title                          転生したらスライムだった件(26)

この2つはちょうどランキングの上2つにあったのものをそのまま使いました。小説もランキングトップ2でやってみます。

> pwsh test.ps1 最推しの義兄

Name                           Value
----                           -----
author                         朝陽天満
itemUrl                        https://books.rakuten.co.jp/rk/97d98e7f207f3528b64bf545fe16b5c9/
koboGenreId                    101940001/101901006/101903

salesDate                      2024年06月07日
seriesName                     最推しの義兄を愛でるため、長生きします!
title                          最推しの義兄を愛でるため、長生きします!4


> pwsh test.ps1 成瀬は天下

Name                           Value
----                           -----
author                         宮島未奈
itemUrl                        https://books.rakuten.co.jp/rk/cfcde0705b423de6a89fa1cf5652e5b3/
koboGenreId                    101901
salesDate                      2023年03月17日
seriesName                     「成瀬」シリーズ
title                          成瀬は天下を取りにいく

「成瀬」は一年前の出版でランキング2位というのはすごいかもしれませんね。他はどれも2024/6月発売なので。

さておき、発刊されている(これからされる)最新刊を知る、ということはこれでできるようにりました!

全体を考える

最新刊の検索はできるようになったので、通知をする、という観点で全体をもういちど考えます。

  • 原則、毎日、毎週など定期的に実行する
  • 最新刊の案内が欲しいのは1作品だけではないので、案内の対象とする作品をリスト化して全部処理する
  • 一度通知したものは覚えておいてさらに次の巻が出るまで通知しない

リストは CSV にしておけば Excel でメンテできて楽そうです。

検索部分を Cmdlet にして通知部分とパイプラインでつなげて使えるようにすると融通も聞くしカッコいいですが、実際の利用シーンはスケジューラでの定期的な自動起動になるのでパイプラインにする意味があまりありません。メソッド呼び出しにしましょう。

リストの読み込み

CSV にしたので Import-Csv で終わり、とはいえ、列名を変えたくなることもあるでしょうし、その都度コードのあちこちを変えるのもどうなの、ということで一捻りしておきます。

Import-Csv -Header でできる? あれは位置依存ですし、Export-Csv にはないですし。

Send-NewBookNotice.ps1
#
# CSV ヘッダー定義
#
class BookListHeader {
    static $Title = ”タイトル";
    static $Genre = "ジャンル";
    static $Keyword = "キーワード";
    static $SalesDate = "最新刊発売日";
}

#
# CSV から詮索リストをロード
#
$csvdata = Import-Csv $script:BookList -Encoding OEM | ForEach-Object { @{
        Title = $_.[BookListHeader]::Title;
        Genre = $_.[BookListHeader]::Genre;
        Keyword = $_.[BookListHeader]::Keyword;
        SalesDate = [DateTime]($_.[BookListHeader]::SalesDate ? $_.[BookListHeader]::SalesDate : 0);
    }
}

残念なことに PowerShell では enum の値を文字列にすることができないので、クラス変数でやります。見た目は enum っぽいのも良しです。これを使って CSV ファイルの列名を定義し、コードで使う名前に置き換えます。

最新刊の検索

Send-NewBookNotice.ps1
#
# Kobo API で最新刊を検索、未知のもののみをピックアップ
#
$api = [KoboAPI]::New()
$api.Init($script:AppId)

$newbooklist = @()
foreach ($item in $csvdata) {
    if ($b = $api.SearchKobo($item.Title, $item.Genre, $item.Keyword)) {
        if ([DateTime]$b.salesDate -gt $item.SalesDate) {
            $newbooklist += ,$b
        }
    } else {
        write-host "Cannot find $($item.Title)"
    }
}

CSV リストの各行に対して API を呼んで検索します。CSV データには前回検索した時の発売日または 0 が入っているので、それよりも新しいものだけを残します。

CSV データの更新(まだ書き出さない)

新規のものが検索できたなら、発売日情報をデータ上で上書きしておきます。通知でコケることも想定してまだファイルは更新しません。

Send-NewBookNotice.ps1
if ($newbooklist) {
    #
    # CSVデータの最新刊発売日を更新
    #
    $newbooklist | ForEach-Object {
        $newbook = $_;
        $item = $csvdata | Where-Object { $newbook.title -match "^$($_.Title)" }
        $item.SalesDate = $newbook.salesDate
    }

通知を出す

モノリシックにすることにしたので、通知部分もこの ps1 の中でやりますが、クラス化して外だしして
います。パラメータを設定して Send() するだけのお手軽仕様。

Send-NewBookNotice.ps1
    #
    # 案内通知
    #
    $params = [NotifyMailParams]::New()
    $params.To = $script:MailTo
    $params.Subject = "楽天Kobo続刊のお知らせ"
    $params.NewBookList = $newbooklist
    $notifyer = [NotifyMail]::New($params)
    $notifyer.Send()

あとかたずけ

CSV ファイルを更新して終わりですが、念のため、ファイルをローテーションさせてバックアップします。

その後、CSV を書き出して終わり。

Send-NewBookNotice.ps1
    #
    # CSV ファイルを更新
    #
    # まずはバックアップ
    3 .. 0 |ForEach-Object {
        $n = $_;
        $n1 = $_ + 1;
        Move-Item -Path "$BookList.$n" -Destination "$BookList.$n1" -Force -ErrorAction SilentlyContinue
    }
    Move-Item -Path "$BookList" -Destination "$BookList.0" -Force -ErrorAction SilentlyContinue

    #
    # 保存
    $csvdata |ForEach-Object {
        @{
            [BookListHeader]::Title = $_.Title;
            [BookListHeader]::Genre = $_.Genre;
            [BookListHeader]::Keyword = $_.Keyword;
            [BookListHeader]::SalesDate = $_.SalesDate;
        }
    } |Select-Object ([BookListHeader]::Title),([BookListHeader]::Genre),([BookListHeader]::Keyword),([BookListHeader]::SalesDate) |
        Export-Csv -Path $BookList -Encoding oem
}

長くなってきたので、実際の通知部分はその3で…。

Discussion