|
/* |
|
■ 使い方 |
|
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.' ); |
|
|
|
} )(); |