Skip to content

Instantly share code, notes, and snippets.

@furyutei
Last active June 23, 2023 08:24
Show Gist options
  • Save furyutei/f7e407fe4b83fd45ef5efbd64ae1a70e to your computer and use it in GitHub Desktop.
Save furyutei/f7e407fe4b83fd45ef5efbd64ae1a70e to your computer and use it in GitHub Desktop.
kindle マンガ本棚の一覧を CSV で取得

kindle マンガ本棚の一覧をCSVで取得する JavaScript

2021/12/25現在、使用不可となっています
いつのまにか
https://www.amazon.co.jp/kindle-dbs/library/manga?ref_=kwrp_m_rd_h_mwl&itemView=cover&sortType=recency
から
https://read.amazon.co.jp/kindle-library/manga?ref_=kwl_m_red
にリダイレクトされるようになり、ページ構造も変わっているようです。
2021/12/26に修正しました(ただし、取得できる情報の追加や削除があった関係もあり、ダウンロードされるCSVのフォーマットも変わっています


使い方

  1. kindle マンガ本棚をブラウザで開く
  2. [F12] を押して、デベロッパー ツール(開発ツール)を開き、Console タブをクリック
  3. このスクリプトをコピーし(右上の [Raw] ボタンを押して開いた画面を Ctrl+A, Ctrl+C するのが楽)、Console に貼り付けて(Ctrl+V)実行([Enter])
  4. しばらく待つと CSV ファイルがダウンロードされる、はず
/*
■ 使い方
1. kindle マンガ本棚
https://read.amazon.co.jp/kindle-library/manga?ref_=kwl_m_red
をブラウザで開く
2. [F12] を押して、デベロッパー ツール(開発ツール)を開き、Console タブをクリック
3. このスクリプトをコピーし(右上の [Raw] ボタンを押して開いた画面を Ctrl+A, Ctrl+C するのが楽)、Console に貼り付けて(Ctrl+V)実行([Enter])
4. しばらく待つと CSV ファイルがダウンロードされるはず
*/
( async () => {
'use strict';
const
AUTHORS_INTO_ONE_COLUMN = true, // true: 作者名をCSVの1カラム(1列)に収める
MAX_QUERY_COUNT = 0, // 最大クエリ発行数(0: 制限なし)
QUERY_SIZE = 50, // 1クエリ辺りの書籍数
itemsListKeys = [
'asin', // ASIN
'title', // 書籍タイトル
'detailPageUrl', // Amazon 製品ページ URL[^1]
'webReaderUrl', // Kindle Cloud Reader(kindle マンガ)URL
'productUrl', // 表紙画像 URL
'seriesAsin', // シリーズASIN
'seriesNumber', // シリーズ内番号
'seriesUrl', // シリーズURL
//'acquisitionDate', // 購入日[^1]
//'lastAnnotationDate', // ???(試した範囲では 0 固定)[^1]
'percentageRead', // ???(試した範囲では 0 固定)[^1]
'authors', // 作者一覧[^2]
],
// [^1] 2021/12から(マンガ本棚のURLが変わってから)入らなくなった
// [^2] 2021/12から(マンガ本棚のURLが変わってから)は配列の0のみに"作者1:作者2:…作者N:"のように入る謎仕様に変わった
authors_key_name = 'authors',
date_key_set = new Set( [ 'acquisitionDate', 'lastAnnotationDate', ] ),
url_key_set = new Set( [ 'detailPageUrl', 'webReaderUrl', 'productUrl', ] ),
series_key_set = new Set( [ 'seriesAsin', 'seriesNumber', 'seriesUrl', ] ),
sortType = 'acquisition_asc', // 'acquisition_asc': 購入日昇順 / 'acquisition_desc': 購入日降順 / 'recency': 最近使用した順
index_url = 'https://read.amazon.co.jp/kindle-library/manga?ref_=kwl_m_red&sortType=' + encodeURIComponent( sortType ),
acquired_api_url_template = 'https://read.amazon.co.jp/kindle-library/search?query=&libraryType=#LIBRARY_TYPE#&paginationToken=#PAGINATION_TOKEN#&sortType=' + encodeURIComponent( sortType ) + '&querySize=' + encodeURIComponent( '' + QUERY_SIZE ),
detailPageUrl_template = 'https://www.amazon.co.jp/gp/product/#ASIN#?pd_rd_i=#ASIN#&storeType=ebooks',
seriesUrl_template = 'https://www.amazon.co.jp/dp/#ASIN#',
csv_filename_template = 'kindle-manga-acquisition-list_#TIMESTAMP#.csv',
get_html_fragment = ( () => {
let html_document = document.implementation.createHTMLDocument(''),
range = html_document.createRange();
return function ( html ) {
return range.createContextualFragment( html );
};
} )(),
normalize_url = ( source_url ) => /^https?:\/\//.test( source_url ) ? source_url : new URL( source_url, location.href ).href,
get_date_string = timestamp_ms => timestamp_ms ? ( new Date( timestamp_ms ).toISOString().replace( /-/g, '/' ) ).replace( /[TZ]/g, ' ' ).trim() : '-',
create_csv_line = values => values.map( value => /^\d+$/.test( value ) ? value : '"' + value.replace( /"/g, '""' ) + '"' ).join( ',' ),
download_csv = ( csv_filename, csv_lines ) => {
let csv_text = csv_lines.join( '\r\n' ),
bom = new Uint8Array( [ 0xEF, 0xBB, 0xBF ] ),
blob = new Blob( [ bom, csv_text ], { 'type' : 'text/csv' } ),
blob_url = URL.createObjectURL( blob ),
download_link = document.createElement( 'a' );
download_link.href = blob_url;
download_link.download = csv_filename;
document.documentElement.appendChild( download_link );
download_link.click();
download_link.remove();
};
let index_html = await fetch( index_url + '&_=' + Date.now() ).then( response => response.text() ),
// TODO: (同一URLの)一覧ページにてそのまま取得するとエラー発生 (Error logged with the Track&Report JS errors API ... [CSM] Ajax request to same page detected fetch : ...)
// → タイムスタンプを付けて違う URL にすることで対応
first_document = get_html_fragment( index_html ),
itemViewResponse = JSON.parse( first_document.querySelector( '#itemViewResponse' ).textContent ),
//validation_token = JSON.parse( first_document.querySelector( '#validation-token' ).textContent ).token,
validation_token = 'undefined',
itemsList = [].concat( itemViewResponse[ 'itemsList' ] );
console.log( 'itemViewResponse:', itemViewResponse );
console.log( '* next paginationToken:', itemViewResponse.paginationToken );
let query_count = 0;
while ( itemViewResponse.paginationToken ) {
let acquired_api = acquired_api_url_template
.replace( /#PAGINATION_TOKEN#/g, encodeURIComponent( itemViewResponse.paginationToken ) )
.replace( /#LIBRARY_TYPE#/g, encodeURIComponent( itemViewResponse.libraryType ) ) +
'&_=' + Date.now();
itemViewResponse = await fetch( acquired_api, {
headers : {
'validation-token' : validation_token,
}
} ).then( response => response.json() );
let resposneItemList = itemViewResponse[ 'itemsList' ].map( item => {
if ( ! item.detailPageUrl ) {
item.detailPageUrl = detailPageUrl_template.replace( /#ASIN#/g, item.asin );
}
if ( ! Array.isArray( item.authors ) ) {
item.authors = [ '' + item.authors ];
}
item.authors = item.authors.reduce( ( author_list, author_text ) => {
author_text.split( /:/ ).filter( author => 0 < author.length ).map( author => author_list.push( author ) );
return author_list;
}, [] );
return item;
} );
itemsList = itemsList.concat( resposneItemList );
console.log( 'itemViewResponse:', itemViewResponse );
console.log( '* next paginationToken:', itemViewResponse.paginationToken );
query_count ++;
if ( ( 0 < MAX_QUERY_COUNT ) && ( MAX_QUERY_COUNT <= query_count ) ) {
break;
}
}
console.log( itemsList.length, 'items found', itemsList );
let max_author_number = itemsList.reduce( ( accumulator, currentValue ) => Math.max( accumulator, currentValue.authors.length ), 1 ),
current_timestamp = ( () => {
var current_datetime = new Date();
current_datetime.setMinutes( current_datetime.getMinutes() - current_datetime.getTimezoneOffset() );
return current_datetime.toISOString().replace(/[\-:Z]/g, '').replace( 'T', '_' );
} )(),
csv_filename = csv_filename_template.replace( /#TIMESTAMP#/g, current_timestamp ),
csv_header = create_csv_line( itemsListKeys.reduce( ( accumulator, currentValue ) => {
accumulator.push( currentValue );
if ( currentValue == authors_key_name ) {
if ( ! AUTHORS_INTO_ONE_COLUMN ) {
accumulator = accumulator.concat( Array( max_author_number - 1 ).fill( '' ) ); // 最大作者数分列を拡張
}
}
return accumulator;
}, [] ) ),
csv_lines = [ csv_header ].concat( itemsList.map( item => {
return create_csv_line( itemsListKeys.reduce( ( accumulator, currentValue ) => {
if ( currentValue == authors_key_name ) {
if ( AUTHORS_INTO_ONE_COLUMN ) {
accumulator.push( item[ currentValue ].join( ', ' ) );
}
else {
accumulator = accumulator.concat( item[ currentValue ] );
accumulator = accumulator.concat( Array( max_author_number - item[ currentValue ].length ).fill( '' ) ); // 最大作者数分列を拡張
}
}
else if ( series_key_set.has( currentValue ) ) {
let parentSeriesInfo = item[ 'parentSeriesInfo' ];
if ( ! parentSeriesInfo ) {
accumulator.push( '' );
}
else {
switch ( currentValue ) {
case 'seriesAsin' :
accumulator.push( parentSeriesInfo.asin );
break;
case 'seriesNumber' :
accumulator.push( parentSeriesInfo.positionInSeries );
break;
case 'seriesUrl' :
accumulator.push( seriesUrl_template.replace( /#ASIN#/g, encodeURIComponent( parentSeriesInfo.asin ) ) );
break;
}
}
}
else if ( date_key_set.has( currentValue ) ) {
accumulator.push( get_date_string( item[ currentValue ] ) );
}
else if ( url_key_set.has( currentValue ) ) {
accumulator.push( normalize_url( item[ currentValue ] ) );
}
else {
accumulator.push( item[ currentValue ] );
}
return accumulator;
}, [] ) );
} ) );
download_csv( csv_filename, csv_lines );
console.log( 'done.' );
} )();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment