Amazonの商品検索ウィジェットが文字化けするので自作してみた part2
3週間ほど前、「Amazonの商品検索ウィジェットが文字化けするので自作してみた」と題して自作のAmazon商品検索スクリプトを公開しました。
当ブログは月間10万人くらいのユニークアクセスがあるのですが、なにしろ「思いついたことを思いついたときに書く」をコンセプトとした、とっちらかっているブログであり、スクリプトを公開しても使う人なんておらんやろ、と正直油断しておりました。
…が、実際はお使い頂いている方がいらっしゃったようでコメント欄にて文字コードに関する問題をご連絡頂いたため、修正版をアップしようと考えた次第です。
前置きの解説がかなーり長くなってしまったので、修正コードがとっとと見たい方は目次の「4 修正版:自作Amazon商品検索ウィジェット」からどうぞ。
前回公開したスクリプトの問題点
UTF-8以外のページからの呼び出しでは動作しない
自分用に作ったスクリプトだったこともあり、お恥ずかしながらかなりテキトーでして、スクリプトの使用例ではURLエンコードすらしておりませんでした。
* 使用例:
* <script src=”amazon_search.php?keywords=ほにゃらら”></script>
見る人が見れば、「このひとURLに日本語そのまま入れてるゥー!プギャーwww」と笑われてしまうようなアレなんですが、いやだってこのほうが見やすいし、ぼくの用途ではほとんど型番(英数字)を使うし、更に言えばUTF8のページからUTF8のPHPスクリプトを呼び出す場合においてはまず問題にならないはず!…という、まさにやっつけ仕事でございました(*ノェノ)キャー
しかし、スクリプトを公開するにあたってはせめて注意事項くらい書くべきでしたよね…。コメント欄で動作しないとの連絡を頂き、ぼく以外に使ってる人いるのか!と驚いた次第です。
すでに前回記事のPHP中には下記のような取り急ぎの修正は入れておりますので、これで文字コードが違うページ間でも一応動くとは思います。(但しPHPスクリプトはUTF8限定)
//2020-05-27 UTF-8以外で渡されることを考慮して追記
$keywords = mb_convert_encoding($keywords, 'UTF-8', 'auto');
しかし、PHPの自動変換に任せるのはちょっぴり不安ではありますよね。
非同期処理が出来ない
更にもうひとつの問題点として非同期処理が出来ないという点が挙げられます。
例えば、以下のように拙作のAmazon商品検索ウィジェットを3つ貼り付けた場合。
<script src="amazon_search.php?keywords=テスト1"></script>
<script src="amazon_search.php?keywords=テスト2"></script>
<script src="amazon_search.php?keywords=テスト3"></script>
この3つの検索結果が終わるまでページ自体が表示されません。
当たり前のことを言っているように聞こえるでしょうか。でも本来、JavaScriptは出来るだけ非同期処理されることが推奨されており、本家の商品検索ウィジェットも該当ページが表示された後、遅れて商品の検索結果が表示されますよね。
しかし仮に非同期実行を実現するために async 属性を付けたとしても、この自作スクリプトの場合エラーになります。
<script src="amazon_search.php?keywords=テスト1" async></script>
これはHTML側の可読性を優先するあまり、document.writeを使って商品の検索結果を表示しているためです。
JavaScriptのdocument.writeはすぐその場に文字を出力するため、非同期実行が出来ません。そりゃそうですよね。ページがすべて描画された後に呼び出されても、どこに文字を挿入するかわからなくなるからです。
実際、XmlHttpRequestなどで非同期呼び出しをして、onload後にdocument.writeをやろうものなら、出力結果が上書きされてdocument.writeした文字以外なにも表示されない、なんてことにもなりかねません。
文字コードの違いによる問題をサンプルで見る
PHPスクリプトのテストサンプル
UTF-8で保存した下記のようなPHPスクリプト(sample1.php)を用意します。
<?php
echo "document.write('".$_GET['keywords']."');";
?>
HTML側のテストサンプル
<html>
<head><meta charset="Shift_JIS"><title>ShiftJISテスト</title></head>
<body>
<h1>ShiftJISのページからUTF8のPHPを呼び出すテスト</h1>
<p>送信する文字:「テスト1」「テスト2」「テスト3」</p>
<h2>テストケース1</h2>
<p><script src="sample1.php?keywords=テスト1"></script></p>
<p><script src="sample1.php?keywords=テスト2"></script></p>
<p><script src="sample1.php?keywords=テスト3"></script></p>
</body>
</html>
実行結果
当たり前と言えば当たり前ですが、このように文字化けします。
呼び出し元のページがShift_JISであり、URLには日本語が書かれているため、ブラウザはそのままShift_JISでパラメータをPHPへ受け渡し、PHP側はそれをUTF-8と考えて処理するため、こんなことが起こるわけですね。
JavaScriptでURLエンコードをして非同期で文字を渡す例
PHPスクリプトのテストサンプル
UTF-8で保存した下記のようなPHPスクリプト(sample2.php)を用意します。
<?php
echo $_GET['keywords'];
?>
HTML側のテストサンプル
<html>
<head><meta charset="Shift_JIS"><title>ShiftJISテスト</title></head>
<body>
<h1>ShiftJISのページからUTF8のPHPを呼び出すテスト</h1>
<p>送信する文字:「テスト1」「テスト2」「テスト3」</p>
<h2>テストケース2</h2>
<p class="amazon-search" data-keywords="テスト1"></p>
<p class="amazon-search" data-keywords="テスト2"></p>
<p class="amazon-search" data-keywords="テスト3"></p>
<script>
document.querySelectorAll('.amazon-search').forEach(e => {
let xhr = new XMLHttpRequest();
xhr.open('GET', 'sample2.php?keywords=' + encodeURIComponent(e.dataset.keywords), true);
xhr.onload = function () { e.innerHTML = this.responseText; }
xhr.send();
});
</script>
</body>
</html>
実行結果
正常に、テスト1、テスト2、テスト3と表示されるだけなので今回はスクリーンショットは貼り付けませんが、コンセプトは伝わったでしょうか。
encodeURIComponentはそのページがどんな文字コードであろうと、UTF-8エンコードしたURLを生成します。なので、検索したい文字だけエンコードして渡せば良いわけですが、最初の書き方であるscriptのsrcにPHPを指定するやり方だとどうしようもないため、PHPスクリプト側でdocument.writeを使うこと自体をやめています。
先述したとおり、document.writeでは非同期実行が出来ない問題もありますしね。
次にHTML側の小細工ですが、- Amazonの商品検索ウィジェットは1ページ内に複数貼りたいケースもある
- PageSpeed Insightsのスコアを上げるためにもJavaScriptの実行は非同期にしたい
という要望を満たすため、
- クラス名 amazon-search を指定した要素の中に検索結果を表示する。
- 検索する文字はカスタムデータ属性 data-keywords で指する。
という仕様にしてみました。
クラス名の指定にしたのはIDをいちいち考えるのが面倒だったからです…w
だって、検索結果を3つ貼りたいからって、id="amazon_search1" id="amazon_search2" id="amazon_search3" とか書きたくないじゃないですか。
関数を用意して、各所に <script>viewAmazonSearch('テスト1');</script> みたいに貼り付ける例も考えましたが、既存のページにある関数名とかぶったりしないか気を使うのも嫌だったので、無名関数を使うことにしました。(forEach~の部分)
これでページが表示し終わった後にXMLHttpRequestが実行され、PHPスクリプトから戻ってきた値をクラス名 amazon-search を指定した要素に入れる、という動作になりました。
修正版:自作Amazon商品検索ウィジェット
ちょっと余談が長くなり過ぎましたが、前回記事で紹介した例だと「文字コードの問題」と「非同期処理の問題」があることだけご理解頂けていれば十分です。
PHPスクリプト
こちらはさほど変更点は多くありません。
document.writeの廃止とstyleタグの出力を廃止したくらいです。
<?php
/*****************************************************************************************
* 修正版 Amazon商品検索ウィジェット
*
*****************************************************************************************/
///////////////////////////////////////////////////////////////////////////////////////////
// 設定部分
///////////////////////////////////////////////////////////////////////////////////////////
//Amazon AWSの設定
$cfg['AWSAccessKeyId'] = "[アクセスキー]";
$cfg['AWSSecretAccessKeyId'] = "[シークレットアクセスキー]";
$cfg['AssociateTag'] = "[アソシエイトタグ]";
//キャッシュ用データベース設定
$cfg['DB_FILE'] = 'amazon_search.db';
$cfg['DB_OPTION'] = array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_TIMEOUT => 20);
//キャッシュ保持日数
$cfg['CACHE_TERM'] = "-3 days";
//検索結果の上部に表示するAmazonアイコン画像
$cfg['AMAZON_ICON'] = 'assocbutt_or_amz.png';
///////////////////////////////////////////////////////////////////////////////////////////
//ここでは$_SERVER['HTTP_REFERER']をチェックする等、特定URL(運営サイト)からのアクセスしか受け付けないようにしたほうが良いかも
if (!isset($_GET['keywords'])) {
exit();
}
//複数サイトを運営していてAmazon商品検索ウィジェットは別ドメインに置きたい場合等は
//Access-Control-Allow-Originの設定が必要
//header('Access-Control-Allow-Origin:https://example.com');
$keywords = $_GET['keywords']; //←必要に応じてサニタイズ処理を
//キャッシュ用データベースの準備
$pdo = connectDB();
//キャッシュ取得または生成
genAmazonCache($pdo, $keywords);
//キャッシュから検索結果を表示
viewAmazonResult($pdo, $keywords);
/************************************************
* データベースを開く
* なければないで作る
************************************************/
function connectDB()
{
global $cfg;
//キャッシュ用のDBファイルがない場合は作る
if (!file_exists($cfg['DB_FILE'])) {
$pdo = new PDO('sqlite:'.$cfg['DB_FILE'], '', '', $cfg['DB_OPTION']);
//キャッシュ用テーブルの準備(1つはキーワードと更新日の保存用、もう1つは商品データ用)
$query = "CREATE TABLE 'amazon_search_keywords' (".
" keywords text PRIMARY KEY,".
" update_date datetime);";
$pdo->query($query);
$query = "CREATE TABLE 'amazon_search_item' (".
" keywords text,".
" asin text,".
" title text,".
" brand text,".
" model text,".
" image_s text,".
" image_m text,".
" image_l text,".
" feature text,".
" price integer);";
$pdo->query($query);
//インデックスも張っておく
$query = "CREATE INDEX amazon_search_item_index ON amazon_search_item(keywords)";
$pdo->query($query);
} else {
$pdo = new PDO('sqlite:'.$cfg['DB_FILE'], '', '', $cfg['DB_OPTION']);
}
return $pdo;
}
/************************************************
* キャッシュをチェックし、なければ(あるいは古ければ)
* Amazonからデータをひっぱってきてキャッシュを作る感じ
************************************************/
function genAmazonCache($pdo, $keywords)
{
global $cfg;
//キーワードキャッシュがあるか確認
$stmt = $pdo->prepare('SELECT update_date FROM amazon_search_keywords WHERE keywords=?');
$stmt->bindValue(1, $keywords);
$stmt->execute();
//更新フラグ
$flg_update = false;
if ($row = $stmt->fetch()) {
if ($row['update_date'] <= date("Y-m-d H:i:s", strtotime($cfg['CACHE_TERM']))) {
//キャッシュがあったとしても一定期間以上経っていた場合は更新する
$flg_update = true;
$stmt = $pdo->prepare('UPDATE amazon_search_keywords SET update_date=? WHERE keywords=?');
$stmt->bindValue(1, date('Y-m-d H:i:s'));
$stmt->bindValue(2, $keywords);
$stmt->execute();
}
} else {
$flg_update = true;
$row = array();
$row['keywords'] = $keywords;
$row['update_date'] = date('Y-m-d H:i:s');
pdoInsert($pdo, 'amazon_search_keywords', $row);
}
if ($flg_update) {
//PAAPIからJSON取得
$json = getAmazonSearchItems($keywords);
$aws = json_decode($json);
$pdo->beginTransaction();
//キャッシュのクリア
$stmt = $pdo->prepare('DELETE FROM amazon_search_item WHERE keywords=?');
$stmt->bindValue(1, $keywords);
$stmt->execute();
//キャッシュに格納
setAmazonItem($pdo, $keywords, $aws);
$pdo->commit();
}
}
/************************************************
* 検索結果を表示
*
************************************************/
function viewAmazonResult($pdo, $keywords)
{
global $cfg;
$stmt = $pdo->prepare('SELECT * FROM amazon_search_item WHERE keywords=?');
$stmt->bindValue(1, $keywords);
$stmt->execute();
$html = '';
$html .= '<div class="amazon_search">';
$search_url = 'https://www.amazon.co.jp/s/ref=as_li_ss_tl?k='.urlencode($keywords).'&tag='.$cfg['AssociateTag'];
$html .= '<a href="'.$search_url.'"><img src="'.$cfg['AMAZON_ICON'].'"></a> <a href="'.$search_url.'">'.htmlspecialchars($keywords).'</a><br>';
$html .= '<section style="display:inline-block;text-align:center;">';
while ($row = $stmt->fetch()) {
$url = 'https://www.amazon.co.jp/dp/'.htmlspecialchars($row['asin']).'/?tag='.$cfg['AssociateTag'];
$html .= '<div>';
$html .= '<a href="'.$url.'" title="'.htmlspecialchars($row['title']).'" target="_blank">';
$html .= '<img src="'.$row['image_m'].'"><br>';
$html .= '<p>'.htmlspecialchars($row['title']).'</p>';
$html .= '</a>';
$html .= '<p>'.number_format($row['price']).'円</p>';
$html .= '</div>';
}
$html .= '</section>';
$html .= '<footer><a href="'.$search_url.'">もっと見る</a></footer>';
$html .= '</div>';
echo $html;
}
/************************************************
* Amazon PAAPIから取得したデータをDBへ格納
*
************************************************/
function setAmazonItem($pdo, $keywords, $aws)
{
foreach ($aws->SearchResult->Items as $item) {
$row_upd = array();
$row_upd['keywords'] = $keywords;
$row_upd['asin'] = $item->ASIN;
$row_upd['title'] = mb_substr($item->ItemInfo->Title->DisplayValue, 0, 255);
$row_upd['brand'] = @$item->ItemInfo->ByLineInfo->Brand->DisplayValue;
$row_upd['model'] = isset($item->ItemInfo->ManufactureInfo->Model) ? $item->ItemInfo->ManufactureInfo->Model->Label : '';
//画像
$row_upd['image_s'] = $item->Images->Primary->Small->URL;
$row_upd['image_m'] = $item->Images->Primary->Medium->URL;
$row_upd['image_l'] = $item->Images->Primary->Large->URL;
if ($row_upd['image_s'] == '') $row_upd[':image_s'] = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
if ($row_upd['image_m'] == '') $row_upd[':image_m'] = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
if ($row_upd['image_l'] == '') $row_upd[':image_l'] = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
$row_upd['price'] = @$item->Offers->Summaries[0]->LowestPrice->Amount;
pdoInsert($pdo, "amazon_search_item", $row_upd);
}
}
/************************************************
* 連想配列からINSERT文を実行する
* (何度も同じ項目名を書くのが面倒なため)
************************************************/
function pdoInsert($pdo, $tablename, $row)
{
$keys1 = '';
$keys2 = '';
foreach ( $row as $key => $value ) {
$keys1 .= $key . ','; //項目名を列挙
$keys2 .= ':' . $key . ','; //プレースホルダを列挙
}
$keys1 = substr($keys1, 0, -1);
$keys2 = substr($keys2, 0, -1);
$stmt = $pdo->prepare('INSERT INTO '.$tablename.' ('.$keys1.') VALUES ('.$keys2.')');
$stmt->execute($row);
}
/************************************************
* ここから下はAmazon PAAPI 5.0のScratchpadで生成されるコードほぼそのまんまです
* https://webservices.amazon.co.jp/paapi5/scratchpad/index.html
************************************************/
function getAmazonSearchItems($keywords) {
global $cfg;
$serviceName="ProductAdvertisingAPI";
$region="us-west-2";
$accessKey=$cfg['AWSAccessKeyId'];
$secretKey=$cfg['AWSSecretAccessKeyId'];
$payload="{"
." \"Keywords\": \"{$keywords}\","
." \"PartnerTag\": \"{$cfg['AssociateTag']}\","
." \"PartnerType\": \"Associates\","
." \"Marketplace\": \"www.amazon.co.jp\","
." \"Resources\": ["
." \"Images.Primary.Small\","
." \"Images.Primary.Medium\","
." \"Images.Primary.Large\","
." \"Offers.Summaries.LowestPrice\","
." \"ItemInfo.Title\","
." \"ItemInfo.ByLineInfo\","
." \"ItemInfo.ManufactureInfo\","
." \"ItemInfo.ProductInfo\""
." ]"
."}";
$host="webservices.amazon.co.jp";
$uriPath="/paapi5/getitems";
$awsv4 = new AwsV4 ($accessKey, $secretKey);
$awsv4->setRegionName($region);
$awsv4->setServiceName($serviceName);
$awsv4->setPath ($uriPath);
$awsv4->setPayload ($payload);
$awsv4->setRequestMethod ("POST");
$awsv4->addHeader ('content-encoding', 'amz-1.0');
$awsv4->addHeader ('content-type', 'application/json; charset=utf-8');
$awsv4->addHeader ('host', $host);
$awsv4->addHeader ('x-amz-target', 'com.amazon.paapi5.v1.ProductAdvertisingAPIv1.SearchItems');
$headers = $awsv4->getHeaders ();
$headerString = "";
foreach ( $headers as $key => $value ) {
$headerString .= $key . ': ' . $value . "\r\n";
}
$params = array (
'http' => array (
'header' => $headerString,
'method' => 'POST',
'content' => $payload
)
);
$stream = stream_context_create ( $params );
$fp = @fopen ( 'https://'.$host.$uriPath, 'rb', false, $stream );
if (! $fp) {
throw new Exception ( "Exception Occured" );
}
$response = @stream_get_contents ( $fp );
if ($response === false) {
throw new Exception ( "Exception Occured" );
}
return $response;
}
class AwsV4 {
private $accessKey = null;
private $secretKey = null;
private $path = null;
private $regionName = null;
private $serviceName = null;
private $httpMethodName = null;
private $queryParametes = array ();
private $awsHeaders = array ();
private $payload = "";
private $HMACAlgorithm = "AWS4-HMAC-SHA256";
private $aws4Request = "aws4_request";
private $strSignedHeader = null;
private $xAmzDate = null;
private $currentDate = null;
public function __construct($accessKey, $secretKey) {
$this->accessKey = $accessKey;
$this->secretKey = $secretKey;
$this->xAmzDate = $this->getTimeStamp ();
$this->currentDate = $this->getDate ();
}
function setPath($path) {
$this->path = $path;
}
function setServiceName($serviceName) {
$this->serviceName = $serviceName;
}
function setRegionName($regionName) {
$this->regionName = $regionName;
}
function setPayload($payload) {
$this->payload = $payload;
}
function setRequestMethod($method) {
$this->httpMethodName = $method;
}
function addHeader($headerName, $headerValue) {
$this->awsHeaders [$headerName] = $headerValue;
}
private function prepareCanonicalRequest() {
$canonicalURL = "";
$canonicalURL .= $this->httpMethodName . "\n";
$canonicalURL .= $this->path . "\n" . "\n";
$signedHeaders = '';
foreach ( $this->awsHeaders as $key => $value ) {
$signedHeaders .= $key . ";";
$canonicalURL .= $key . ":" . $value . "\n";
}
$canonicalURL .= "\n";
$this->strSignedHeader = substr ( $signedHeaders, 0, - 1 );
$canonicalURL .= $this->strSignedHeader . "\n";
$canonicalURL .= $this->generateHex ( $this->payload );
return $canonicalURL;
}
private function prepareStringToSign($canonicalURL) {
$stringToSign = '';
$stringToSign .= $this->HMACAlgorithm . "\n";
$stringToSign .= $this->xAmzDate . "\n";
$stringToSign .= $this->currentDate . "/" . $this->regionName . "/" . $this->serviceName . "/" . $this->aws4Request . "\n";
$stringToSign .= $this->generateHex ( $canonicalURL );
return $stringToSign;
}
private function calculateSignature($stringToSign) {
$signatureKey = $this->getSignatureKey ( $this->secretKey, $this->currentDate, $this->regionName, $this->serviceName );
$signature = hash_hmac ( "sha256", $stringToSign, $signatureKey, true );
$strHexSignature = strtolower ( bin2hex ( $signature ) );
return $strHexSignature;
}
public function getHeaders() {
$this->awsHeaders ['x-amz-date'] = $this->xAmzDate;
ksort ( $this->awsHeaders );
// Step 1: CREATE A CANONICAL REQUEST
$canonicalURL = $this->prepareCanonicalRequest ();
// Step 2: CREATE THE STRING TO SIGN
$stringToSign = $this->prepareStringToSign ( $canonicalURL );
// Step 3: CALCULATE THE SIGNATURE
$signature = $this->calculateSignature ( $stringToSign );
// Step 4: CALCULATE AUTHORIZATION HEADER
if ($signature) {
$this->awsHeaders ['Authorization'] = $this->buildAuthorizationString ( $signature );
return $this->awsHeaders;
}
}
private function buildAuthorizationString($strSignature) {
return $this->HMACAlgorithm . " " . "Credential=" . $this->accessKey . "/" . $this->getDate () . "/" . $this->regionName . "/" . $this->serviceName . "/" . $this->aws4Request . "," . "SignedHeaders=" . $this->strSignedHeader . "," . "Signature=" . $strSignature;
}
private function generateHex($data) {
return strtolower ( bin2hex ( hash ( "sha256", $data, true ) ) );
}
private function getSignatureKey($key, $date, $regionName, $serviceName) {
$kSecret = "AWS4" . $key;
$kDate = hash_hmac ( "sha256", $date, $kSecret, true );
$kRegion = hash_hmac ( "sha256", $regionName, $kDate, true );
$kService = hash_hmac ( "sha256", $serviceName, $kRegion, true );
$kSigning = hash_hmac ( "sha256", $this->aws4Request, $kService, true );
return $kSigning;
}
private function getTimeStamp() {
return gmdate ( "Ymd\THis\Z" );
}
private function getDate() {
return gmdate ( "Ymd" );
}
}
?>
使用例
JavaScriptとCSSの部分は共通のテンプレートなどの外部ファイルに書いてしまうことで、各ページに貼り付けるコードはdivタグだけでシンプルになるかと思います。
HTML部分
<div class="amazon-search" data-keywords="PHP"></div>
JavaScript部分
<script>
document.querySelectorAll('.amazon-search').forEach(e => {
let xhr = new XMLHttpRequest();
xhr.open('GET', 'amazon_search.php?keywords=' + encodeURIComponent(e.dataset.keywords), true);
xhr.onload = function () { e.innerHTML = this.responseText; }
xhr.send();
});</script>
CSS部分
<style>
div.amazon_search { border:1px solid #cccccc;border-top:3px solid #ff8000;border-radius:4px;padding:4px 1em 0 1em; }
div.amazon_search img:nth-child(1) { height:20px;margin:0 0 14px;vertical-align:top;border:0; }
div.amazon_search a { font-size:9pt;text-decoration:none; }
div.amazon_search a:nth-child(2) { display:inline-block; }
div.amazon_search div { position:relative;display:inline-block;margin:0 6px 2px 0; }
div.amazon_search div img:nth-child(1) { width:120px;height:120px;object-fit:cover; }
div.amazon_search div:hover p { transform:scale(1.2);transition-duration:0.3s; }
div.amazon_search div:hover img { transform:scale(1.2);transition-duration:0.3s; }
div.amazon_search div p { position:absolute;width:120px; }
div.amazon_search div a p { bottom:28px;background-color:rgba(100,100,100,0.3);color:white;margin:1px;font-size:9pt;white-space:nowrap;text-overflow:ellipsis;overflow:hidden; }
div.amazon_search div p { bottom:0px;text-align:right;font-weight:bold;color:#cc0000;font-size:8pt; }
div.amazon_search footer { font-size:0.8em;text-align:right; }
</style>
実行結果
まとめ
以前公開した「Amazonの商品検索ウィジェットが文字化けするので自作してみた」では文字コードの問題が発生する可能性があったのと、非同期処理も出来なかったため、修正版を作ってみた。
という
お話でした。もうちょっと綺麗に書けたんじゃないか…という気もしますが、そこは…ほれ…見た人が良い感じに修正してくれることを期待します…w
尚、当ブログで公開している自作コードについての動作保証は全く出来ませんが、ご自由に改変してお使い頂いて構いません。
2020-05-28追記
SearchIndexやBrowseNodeIdを指定できる機能を追加したバージョンを公開しました。