嗯……我写这干啥??
https://www.biliplus.com/manga/
web用阅读器,给ipad看漫画用的
shit title placeholder |
嗯……我写这干啥??
https://www.biliplus.com/manga/
web用阅读器,给ipad看漫画用的
<?php | |
if (empty($_GET['mangaid']) || !preg_match('(^\d+$)', $_GET['mangaid'])) { | |
errordie('无效id'); | |
} | |
$curl = curl_init(); | |
if (isset($_GET['unlock'])) { | |
$unlocks = explode(',', $_GET['unlock']); | |
unset($_GET['unlock']); | |
$returnUrlParam = http_build_query($_GET); | |
foreach ($unlocks as $id) { | |
$unlockResult = json_decode(cget('http://manga.bilibili.com/twirp/comic.v1.Comic/UnlockComicAlbum', [ | |
'curl' => $curl, | |
'post' => buildParam([ | |
'access_key'=>$_COOKIE['access_key'], | |
'actionKey'=>'appkey', | |
'appkey'=>$appkey, | |
'build'=>APPBUILD, | |
'id'=>$id, | |
'device'=>'phone', | |
'mobi_app'=>'iphone_comic', | |
'platform'=>'ios', | |
'ts'=>time(), | |
'version'=>APPVER | |
], $appsecret) | |
]), true); | |
if ($unlockResult['code'] !== 0) { | |
$msg = '解锁特典'.$id.'出错: ['.$unlockResult['code'].']'.$unlockResult['msg']; | |
break; | |
} | |
} | |
header('Set-Cookie: manga_album_unlock_message='. urlencode(isset($msg) ? $msg : '已解锁') . '; Max-Age=30; path=/manga/; secure; HttpOnly'); | |
header('Location: ?'.$returnUrlParam, true, 302); | |
exit; | |
} | |
chdir(__DIR__); | |
$idxDb = new PDO('sqlite:index.db'); | |
$albums = cget('http://manga.bilibili.com/twirp/comic.v1.Comic/GetComicAlbumPlus', [ | |
'curl' => $curl, | |
'post' => buildParam([ | |
'access_key'=>$_COOKIE['access_key'], | |
'actionKey'=>'appkey', | |
'appkey'=>$appkey, | |
'build'=>APPBUILD, | |
'comic_id'=>$_GET['mangaid'], | |
'device'=>'phone', | |
'mobi_app'=>'iphone_comic', | |
'platform'=>'ios', | |
'ts'=>time(), | |
'version'=>APPVER | |
], $appsecret) | |
]); | |
if (isset($_GET['api'])) { | |
header('Content-Type: application/json; charset="UTF-8"'); | |
echo $albums; exit; | |
} | |
if (!empty($_COOKIE['manga_album_unlock_message'])) { | |
$unlockMessage = $_COOKIE['manga_album_unlock_message']; | |
header('Set-Cookie: manga_album_unlock_message=; Max-Age=0; path=/manga/; secure; HttpOnly'); | |
} | |
$albums = json_decode($albums, true); | |
if ($albums['code'] !== 0) { | |
errordie('获取特典列表出错: ['.$albums['code'].']'.$albums['msg']); | |
} | |
if (empty($albums['data']['list'])) { | |
errordie('本作品无特典'); | |
} | |
$detail = cget('http://manga.bilibili.com/twirp/comic.v2.Comic/ComicDetail', [ | |
'curl' => $curl, | |
'post' => buildParam([ | |
'access_key'=>$_COOKIE['access_key'], | |
'actionKey'=>'appkey', | |
'appkey'=>$appkey, | |
'build'=>APPBUILD, | |
'comic_id'=>$_GET['mangaid'], | |
'device'=>'phone', | |
'mobi_app'=>'iphone_comic', | |
'platform'=>'ios', | |
'ts'=>time(), | |
'version'=>APPVER | |
], $appsecret) | |
]); | |
$detail = json_decode($detail, true); | |
if ($detail['code'] !== 0) { | |
errordie('获取漫画信息出错: ['.$detail['code'].']'.$detail['msg']); | |
} | |
$detail = $detail['data']; | |
$epMap = []; | |
$epUnlockedList = []; | |
foreach ($detail['ep_list'] as $ep) { | |
$epMap[$ep['id']] = [!$ep['is_locked'], $ep['short_title']]; | |
} | |
$batchUnlock = []; | |
$albumInfoEmptyStmt = $idxDb->prepare('INSERT OR IGNORE INTO album_info (id,type,manga_id,pic_hash,title,detail) VALUES (?,?,?,0,?,?)'); | |
$albumInfoStmt = $idxDb->prepare('REPLACE INTO album_info (id,type,manga_id,pic_hash,title,detail) VALUES (?,?,?,?,?,?)'); | |
$picDataInsertStmt = $idxDb->prepare('INSERT OR IGNORE INTO album_pic_data (album_id,hash,data) VALUES (?,?,CAST(? AS BLOB))'); | |
$idxDb->beginTransaction(); | |
foreach ($albums['data']['list'] as &$album) { | |
if ($album['isLock']) { | |
$albumInfoEmptyStmt->execute([$album['item']['id'], $album['item']['type'], $_GET['mangaid'], $album['item']['title'], $album['item']['detail']]); | |
if (in_array($album['item']['type'], [4, 5])) { | |
$unlockCount = 0; $remaining = []; | |
foreach ($album['item']['item_infos'] as $requiredItem) { | |
if (in_array($requiredItem['id'], $album['unlocked_item_ids']) !== false) $unlockCount++; | |
else $remaining[] = $requiredItem['title']; | |
} | |
$album['__unlockCount'] = $unlockCount; | |
$album['__remaining'] = $remaining; | |
if (empty($remaining)) $batchUnlock[] = $album['item']['id']; | |
} | |
} else { | |
$picPaths = array_map(function ($p) { | |
preg_match('(//[^/]+([^\?@]+))', $p, $m); | |
return $m[1]; | |
}, $album['item']['pic']); | |
/** | |
* 231224 | |
* /bfs/mangav4.local.uD4Jv3FO2ExO8jhMBK1VketDPVVP8z0bf9-R4bT07hiC1j8jk0sIwggTvU7W_dNODcnmqSG9lRBo52sqFfdsA7k63YhN7LAIqZsa6qBPvXg5B_u6RUKETP6oDupi_JGjvaJ5nxyzxhyW_JNRAu2FW6rSME8Ko6c_7jwu9ImRkHG2t389zJanJRxAFjCGFmdapcfdrMlKojYz8KW7Zrg-EcAmeFqhdR11lA/e5f24a7cff6da6b61eebea5682c19d09d08e6681.jpg | |
* 路径替换为 | |
* /bfs/manga/e5f24a7cff6da6b61eebea5682c19d09d08e6681.jpg | |
*/ | |
$picPaths = array_map(function ($a) { | |
return preg_replace('(/mangav4.local\.[^/]+/)', '/manga/', $a); | |
}, $picPaths); | |
$picPaths = json_encode($picPaths, JSON_UNESCAPED_SLASHES); | |
$picPathsData = brotli_compress($picPaths, 4); | |
$hash = crc32($picPathsData); | |
$albumInfoStmt->execute([$album['item']['id'], $album['item']['type'], $_GET['mangaid'], $hash, $album['item']['title'], $album['item']['detail']]); | |
$picDataInsertStmt->execute([$album['item']['id'], $hash, $picPathsData]); | |
} | |
} | |
$idxDb->commit(); | |
unset($album); | |
?> | |
<!DOCTYPE html><html> | |
<head> | |
<meta charset="UTF-8" name="viewport" content="width=device-width,user-scalable=no"> | |
<meta name="format-detection" content="telephone=no" /> | |
<meta name="referrer" content="never"> | |
<title>特典 - <?php echo $detail['title'];?> - 书籍 - bilibili漫画 阅读器 - BiliPlus</title> | |
<script src="materialize.min.js"></script> | |
<link rel="stylesheet" href="materialize.min.css" /> | |
<style> | |
body {width:95%;max-width:700px;margin:5px auto;background:#EFEFF4;cursor:default} | |
.album{clear:both} | |
@media (prefers-color-scheme: dark) { | |
body {background:black;color:#DDD} | |
.card {background:#1c1c1c} | |
img {filter:brightness(0.8)} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="col s12"> | |
<?php if (isset($unlockMessage)) { ?> | |
<div class="card horizontal"> | |
<div class="card-stacked"> | |
<div class="card-content"> | |
<p><?php echo $unlockMessage;?></p> | |
</div> | |
</div> | |
</div> | |
<hr> | |
<?php } ?> | |
<h4>特典列表 - <?php echo $detail['title'];?></h4> | |
<?php | |
if (!empty($batchUnlock)) { | |
?> | |
<p><a href="?<?php echo 'act=album&mangaid='.$_GET['mangaid'].'&unlock='.implode(',', $batchUnlock)?>" class="waves-effect waves-light btn">一键解锁</a></p> | |
<?php | |
} | |
foreach ($albums['data']['list'] as $album) { ?> | |
<div class="album" id="album-<?php echo $album['item']['id']?>"> | |
<hr> | |
<img style="float:right;height:200px" alt="特典预览" src="<?php echo wrapPicUrl(str_replace('http:','',$album['item']['cover']).'@400h.jpg');?>"> | |
<h5><?php echo $album['item']['title']?></h5> | |
<h6><?php echo $album['item']['detail']?>(id <?php echo $album['item']['id']?>)</h6> | |
<p>共 <?php echo $album['item']['pic_num']?> 页,<?php echo $album['item']['online_time']?> 至 <?php echo $album['item']['offline_time']?></p> | |
<?php if ($album['isLock']) { | |
switch ($album['item']['type']) { | |
case 1: { | |
?> | |
<p> | |
<div>解锁条件:在本漫画投喂达<?php echo $album['item']['num']?>漫币获得</div> | |
</p> | |
<p><a href="?<?php echo 'act=album&mangaid='.$_GET['mangaid'].'&unlock='.$album['item']['id']?>" class="waves-effect waves-light btn">解锁</a> <a href="?act=read_album&albumid=<?php echo $album['item']['id']?>" class="waves-effect waves-light btn" target="_manga_view">查看</a></p> | |
<?php break; | |
} | |
case 2: { | |
?> | |
<p> | |
<div>解锁条件:在本漫画消费达<?php echo $album['item']['num']?>漫币获得</div> | |
</p> | |
<p><a href="?<?php echo 'act=album&mangaid='.$_GET['mangaid'].'&unlock='.$album['item']['id']?>" class="waves-effect waves-light btn">解锁</a> <a href="?act=read_album&albumid=<?php echo $album['item']['id']?>" class="waves-effect waves-light btn" target="_manga_view">查看</a></p> | |
<?php break; | |
} | |
case 3: { | |
?> | |
<p> | |
<div>解锁条件:参与活动<a href="<?php echo $album['item']['activity_url']?>" target="_blank"><?php echo $album['item']['activity_name']?></a></div> | |
</p> | |
<p><a href="?<?php echo 'act=album&mangaid='.$_GET['mangaid'].'&unlock='.$album['item']['id']?>" class="waves-effect waves-light btn">解锁</a> <a href="?act=read_album&albumid=<?php echo $album['item']['id']?>" class="waves-effect waves-light btn" target="_manga_view">查看</a></p> | |
<?php break; | |
} | |
case 4: { | |
$unlockRequirement = count($album['item']['item_ids']); | |
$unlockCount = $album['__unlockCount']; | |
$remaining = $album['__remaining']; | |
?> | |
<p> | |
<div>解锁条件:<?php echo "$unlockCount/$unlockRequirement 已购买"?></div> | |
<?php if (!empty($remaining)) { ?> | |
<div>还需购买章节 <?php echo implode('、', $remaining) ?></div> | |
<?php } ?> | |
</p> | |
<p><a href="?<?php echo 'act=album&mangaid='.$_GET['mangaid'].'&unlock='.$album['item']['id']?>" class="waves-effect waves-light btn<?php if (!empty($remaining)) echo ' disabled' ?>">解锁</a> <a href="?act=read_album&albumid=<?php echo $album['item']['id']?>" class="waves-effect waves-light btn" target="_manga_view">查看</a></p> | |
<?php break; | |
} | |
case 5: { | |
$unlockRequirement = count($album['item']['item_ids']); | |
$unlockCount = $album['__unlockCount']; | |
$remaining = $album['__remaining']; | |
?> | |
<p> | |
<div>解锁条件:<?php echo "$unlockCount/$unlockRequirement 已购买"?></div> | |
<?php if (!empty($remaining)) { ?> | |
<div>还需购买 <?php echo implode('、', $remaining) ?></div> | |
<?php } ?> | |
</p> | |
<p><a href="?<?php echo 'act=album&mangaid='.$_GET['mangaid'].'&unlock='.$album['item']['id']?>" class="waves-effect waves-light btn<?php if (!empty($remaining)) echo ' disabled' ?>">解锁</a> <a href="?act=read_album&albumid=<?php echo $album['item']['id']?>" class="waves-effect waves-light btn" target="_manga_view">查看</a></p> | |
<?php break; | |
} | |
default: { | |
?> | |
<p> | |
<div>解锁条件:解锁类型 [<?php echo $album['item']['type']?>]</div> | |
</p> | |
<p><a href="?<?php echo 'act=album&mangaid='.$_GET['mangaid'].'&unlock='.$album['item']['id']?>" class="waves-effect waves-light btn disabled">解锁</a> <a href="?act=read_album&albumid=<?php echo $album['item']['id']?>" class="waves-effect waves-light btn" target="_manga_view">查看</a></p> | |
<?php break; | |
} | |
} | |
} else { ?> | |
<p>已解锁(第<?php echo $album['item']['rank']?>位解锁)</p> | |
<p><a href="?act=read_album&albumid=<?php echo $album['item']['id']?>" class="waves-effect waves-light btn" target="_manga_view">查看</a></p> | |
<?php } ?> | |
</div> | |
<?php } ?> | |
</div> | |
</body> | |
</html> |
<?php | |
if (!ALLOW_SHARING) { | |
errordie('未开启缓存'); | |
} | |
if (empty($_GET['mangaid']) || !preg_match('(^\d+$)', $_GET['mangaid'])) { | |
errordie('无效id'); | |
} | |
chdir(__DIR__); | |
$idxDb = new PDO('sqlite:index.db'); | |
$detail = cget('http://manga.bilibili.com/twirp/comic.v2.Comic/ComicDetail', [ | |
'post' => buildParam([ | |
'access_key'=>$_COOKIE['access_key'], | |
'actionKey'=>'appkey', | |
'appkey'=>$appkey, | |
'build'=>APPBUILD, | |
'comic_id'=>$_GET['mangaid'], | |
'device'=>'phone', | |
'mobi_app'=>'iphone_comic', | |
'platform'=>'ios', | |
'ts'=>time(), | |
'version'=>APPVER | |
], $appsecret) | |
]); | |
$detail = json_decode($detail, true); | |
if ($detail['code'] !== 0) { | |
errordie('获取出错: ['.$detail['code'].']'.$detail['msg']); | |
} | |
$detail = $detail['data']; | |
usort($detail['ep_list'], function ($a, $b) {return $a['ord'] - $b['ord'];}); | |
$store_stmt = $idxDb->prepare('REPLACE INTO index_data (epid,mangaid,hash) VALUES (?,?,?)'); | |
$history = $idxDb->prepare('REPLACE INTO index_data_history (epid,mangaid,hash,data,time,uid) VALUES (?,?,?,CAST(? AS BLOB),?,?)'); | |
$hashChk = $idxDb->prepare('SELECT count(epid) FROM index_data WHERE epid=? AND hash=?'); | |
$epChk = $idxDb->prepare('SELECT count(a.epid) FROM index_data AS a,index_data_history AS b WHERE a.epid=? AND a.epid=b.epid AND a.hash=b.hash'); | |
$epChkStore = $idxDb->prepare('SELECT count(id) FROM episode_index WHERE id=?'); | |
$idxPath = $idxDb->prepare('REPLACE INTO index_path (epid,path) VALUES (?,?)'); | |
$accountKeyStore = $idxDb->prepare('UPDATE account SET accesskey=?, refresh=?, ip=?, keyexpire=? WHERE uid=? AND keyexpire<?'); | |
$recoverExpiredEpisode = $idxDb->prepare('INSERT OR IGNORE INTO index_data_history SELECT epid,mangaid,hash,data,time,uid FROM index_data_expired WHERE uid=?'); | |
$deleteRecoveredEpisode = $idxDb->prepare('DELETE FROM index_data_expired WHERE uid=?'); | |
if ($detail['discount_type'] == 2 && $detail['discount'] == 0) { | |
$purchasedEpList = $detail['ep_list']; | |
} else { | |
$purchasedEpList = array_filter($detail['ep_list'], function ($i) {return $i['is_in_free'] || !$i['is_locked'];}); | |
} | |
header('Content-Encoding: identity'); | |
?> | |
<!DOCTYPE html><html> | |
<head> | |
<meta charset="UTF-8" name="viewport" content="width=device-width,user-scalable=no"> | |
<meta name="format-detection" content="telephone=no" /> | |
<meta name="referrer" content="never"> | |
<title><?php echo $detail['title'];?> - 书籍 - bilibili漫画 阅读器 - BiliPlus</title> | |
<script src="materialize.min.js"></script> | |
<link rel="stylesheet" href="materialize.min.css" /> | |
<style> | |
body {width:95%;max-width:700px;margin:5px auto;background:#EFEFF4;cursor:default} | |
@media (prefers-color-scheme: dark) { | |
body {background:black;color:#DDD} | |
img {filter:brightness(0.8)} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="col s12"> | |
<h3><?php echo $detail['title'];?></h3> | |
<p>共计 <?php echo count($detail['ep_list'])?> 话,可访问共 <?php echo count($purchasedEpList)?> 话</p> | |
<div style="display:flex;flex-direction:column-reverse"><div style="display:flex;flex-direction:column-reverse"> | |
<?php | |
ob_flush(); | |
flush(); | |
include_once 'batch_index_private.php'; | |
$i = 0; | |
$total = count($purchasedEpList); | |
set_time_limit(0); | |
$apiCurl = curl_init(); | |
$idxCurl = curl_init(); | |
foreach ($purchasedEpList as $ep) { | |
$i++; | |
if (isset($_GET['skip_cached'])) { | |
$epChk->execute([$ep['id']]); | |
if ($epChk->fetch()[0] == 1) continue; | |
$epChkStore->execute([$ep['id']]); | |
if ($epChkStore->fetch()[0] == 1) continue; | |
} | |
$epChk->closeCursor(); | |
echo "\n <div>".$i.'/'.$total.' '.$ep['id'].' '.$ep['short_title'].'...'; | |
$idxUrl = json_decode(cget('http://manga.bilibili.com/twirp/comic.v1.Comic/GetImageIndex', [ | |
'post' => buildParam([ | |
'access_key'=>$_COOKIE['access_key'], | |
'actionKey'=>'appkey', | |
'appkey'=>$appkey, | |
'build'=>APPBUILD, | |
'device'=>'phone', | |
'ep_id'=>$ep['id'], | |
'mobi_app'=>'iphone_comic', | |
'platform'=>'ios', | |
'ts'=>time(), | |
'version'=>APPVER, | |
], $appsecret), | |
'curl'=>$apiCurl | |
]), true); | |
if ($idxUrl['code'] !== 0) { | |
echo '获取索引出错: ['.$idxUrl['code'].']'.$idxUrl['msg'].'</div>'; | |
continue; | |
} | |
preg_match('(/manga/(\d+)/(\d+)/data\.index)', $idxUrl['data']['path'], $idMatch); | |
$realmangaid = $idMatch[1]; | |
$realepid = $idMatch[2]; | |
$indexData = $idxUrl['data']['images']; | |
$indexDataStr = json_encode($indexData, JSON_UNESCAPED_SLASHES); | |
$picFileNames = array_map(function ($i) {return pathinfo($i['path'], PATHINFO_BASENAME);}, $indexData); | |
$data = brotli_compress($indexDataStr, 9); | |
$hash = crc32(json_encode($picFileNames)); | |
$hashChk->execute([$ep['id'], $hash]); | |
$updated = $hashChk->fetch()[0] != 1; | |
$hashChk->closeCursor(); | |
$retry = 0; | |
do { | |
try { | |
$idxDb->beginTransaction(); | |
break; | |
} catch (Exception $e) { | |
usleep(250000); | |
if (++$retry > 3) { | |
echo "DB异常</div>"; | |
break 2; | |
} | |
$idxDb = new PDO('sqlite:index.db'); | |
$store_stmt = $idxDb->prepare('REPLACE INTO index_data (epid,mangaid,hash) VALUES (?,?,?)'); | |
$history = $idxDb->prepare('REPLACE INTO index_data_history (epid,mangaid,hash,data,time,uid) VALUES (?,?,?,CAST(? AS BLOB),?,?)'); | |
$hashChk = $idxDb->prepare('SELECT count(epid) FROM index_data WHERE epid=? AND hash=?'); | |
$epChk = $idxDb->prepare('SELECT count(a.epid) FROM index_data AS a,index_data_history AS b WHERE a.epid=? AND a.epid=b.epid AND a.hash=b.hash'); | |
$idxPath = $idxDb->prepare('REPLACE INTO index_path (epid,path) VALUES (?,?)'); | |
$accountKeyStore = $idxDb->prepare('UPDATE account SET accesskey=?, refresh=?, ip=?, keyexpire=? WHERE uid=? AND keyexpire<?'); | |
$recoverExpiredEpisode = $idxDb->prepare('INSERT OR IGNORE INTO index_data_history SELECT epid,mangaid,hash,data,time,uid FROM index_data_expired WHERE uid=?'); | |
$deleteRecoveredEpisode = $idxDb->prepare('DELETE FROM index_data_expired WHERE uid=?'); | |
} | |
} while (1); | |
$result = true; | |
$result = $result && $store_stmt->execute([$realepid, $realmangaid, $hash]); $store_stmt->closeCursor(); | |
$result = $result && $history->execute([$realepid, $realmangaid, $hash, $data, time(), $_COOKIE['mid']]); $history->closeCursor(); | |
$result = $result && $idxPath->execute([$ep['id'], explode('?', $idxUrl['data']['path'])[0]]); $idxPath->closeCursor(); | |
$result = $result && $accountKeyStore->execute([$_COOKIE['access_key'], $_COOKIE['refresh_token'], $_COOKIE['user_ip_enc'], $_COOKIE['expires'], $_COOKIE['mid'], $_COOKIE['expires']]); $accountKeyStore->closeCursor(); | |
$result = $result && $recoverExpiredEpisode->execute([$_COOKIE['mid']]); $recoverExpiredEpisode->closeCursor(); | |
$result = $result && $deleteRecoveredEpisode->execute([$_COOKIE['mid']]); $deleteRecoveredEpisode->closeCursor(); | |
$result = $result && $idxDb->commit(); | |
if (!$result) { | |
echo '写入失败</div>'; | |
continue; | |
} | |
if ($updated) echo '更新'.$idxUrl['data']['last_modified']; | |
echo "完成</div>"; | |
@ob_flush();@flush(); | |
} | |
?> | |
</div> | |
<p><a href="javascript:window.close()" class="col s4 waves-effect waves-light btn">关闭</a></p> | |
</div></div> | |
</body> | |
</html> | |
<?php | |
<?php | |
if (empty($_GET['mangaid']) || !preg_match('(^\d+$)', $_GET['mangaid'])) { | |
errordie('无效id'); | |
} | |
chdir(__DIR__); | |
$idxDb = new PDO('sqlite:index.db'); | |
$detail = cget('http://manga.bilibili.com/twirp/comic.v2.Comic/ComicDetail', [ | |
'post' => buildParam([ | |
'access_key'=>$_COOKIE['access_key'], | |
'actionKey'=>'appkey', | |
'appkey'=>$appkey, | |
'build'=>APPBUILD, | |
'comic_id'=>$_GET['mangaid'], | |
'device'=>'phone', | |
'mobi_app'=>'iphone_comic', | |
'platform'=>'ios', | |
'ts'=>time(), | |
'version'=>APPVER | |
], $appsecret) | |
]); | |
if (isset($_GET['api'])) { | |
header('Content-Type: application/json; charset="UTF-8"'); | |
echo $detail; exit; | |
} | |
$detail = json_decode($detail, true); | |
if ($detail['code'] !== 0) { | |
errordie('获取出错: ['.$detail['code'].']'.$detail['msg']); | |
} | |
$detail = $detail['data']; | |
usort($detail['ep_list'], function ($a, $b) {return $a['ord'] > $b['ord'] ? 1 : -1;}); | |
if ($detail['discount_type'] == 2 && $detail['discount'] == 0) { | |
foreach ($detail['ep_list'] as &$ep) { | |
$ep['is_locked'] = false; | |
} | |
unset($ep); | |
} | |
$idxDb->prepare('REPLACE INTO manga_info (id,title,list) VALUES (?,?,CAST(? AS BLOB))')->execute([ | |
$detail['id'], | |
$detail['title'], | |
brotli_compress(json_encode($detail['ep_list'], JSON_UNESCAPED_UNICODE+JSON_UNESCAPED_SLASHES), 9) | |
]); | |
$discount_desc = []; | |
switch ($detail['discount_type']) { | |
case 0: { break; } | |
case 1: { | |
$discount_desc[] = '全本'.($detail['discount']/10).'折'; | |
break; | |
} | |
case 2: { | |
if ($detail['discount']) { | |
$discount_desc[] = '单章'.($detail['discount']/10).'折'; | |
} else { | |
$discount_desc[] = '限时免费'; | |
} | |
break; | |
} | |
case 3: { | |
$discount_desc[] = '部分限时免费'; | |
break; | |
} | |
} | |
if ($detail['ep_discount_type']) { | |
$discount_desc[] = '单章购买优惠'; | |
} | |
if ($detail['batch_discount_type']) { | |
$discount_desc[] = '批量购买优惠'; | |
} | |
function prettySize(int $s) { | |
if ($s <= 0) return null; | |
$units = ['B','KB','MB','GB']; | |
$unit = 0; | |
if ($s < 1001) return $s.'B'; | |
while ($s > 1000) { | |
$unit++; | |
$s/=1024; | |
} | |
return number_format($s, 2, '.', '') . $units[$unit]; | |
} | |
?> | |
<!DOCTYPE html><html> | |
<head> | |
<meta charset="UTF-8" name="viewport" content="width=device-width,user-scalable=no"> | |
<meta name="format-detection" content="telephone=no" /> | |
<meta name="referrer" content="never"> | |
<title><?php echo $detail['title'];?> - 书籍 - bilibili漫画 阅读器 - BiliPlus</title> | |
<script src="materialize.min.js"></script> | |
<link rel="stylesheet" href="materialize.min.css" /> | |
<style> | |
body {width:95%;max-width:700px;margin:5px auto;background:#EFEFF4;cursor:default} | |
.comic-cover{height:200px} | |
.episode.block{display:inline-block;width:50px;margin:8px 15px;border:1px solid;border-radius:5px;text-align:center;} | |
.episode.locked{color:#666;border-color:#666} | |
.contents-simple .episode.no-index{border-style:dashed} | |
a.episode:visited{color:#0645ad} | |
rt{font-size:70%;color:#888} | |
.multiple-version{position:relative} | |
.multiple-version::after{content:"*";position:absolute} | |
.contents-full{text-align:left} | |
.flex{display:flex} | |
.flex.hoz{flex-direction:horizontal} | |
.flex.ver{flex-direction:vertical} | |
.flex>div{flex:1} | |
.flex>.epid{flex:1.5} | |
.comments::before{content: "";background:url('data:image/svg+xml,%3Csvg height%3D"20" width%3D"25" viewBox%3D"0 0 60 60" version%3D"1.1" xmlns%3D"http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg"%3E%3Cpath d%3D"M12%2C17h15c0.553%2C0%2C1-0.448%2C1-1s-0.447-1-1-1H12c-0.553%2C0-1%2C0.448-1%2C1S11.447%2C17%2C12%2C17z"%2F%3E%3Cpath d%3D"M46%2C23H12c-0.553%2C0-1%2C0.448-1%2C1s0.447%2C1%2C1%2C1h34c0.553%2C0%2C1-0.448%2C1-1S46.553%2C23%2C46%2C23z"%2F%3E%3Cpath d%3D"M46%2C31H12c-0.553%2C0-1%2C0.448-1%2C1s0.447%2C1%2C1%2C1h34c0.553%2C0%2C1-0.448%2C1-1S46.553%2C31%2C46%2C31z"%2F%3E%3Cpath d%3D"M54%2C2H6C2.748%2C2%2C0%2C4.748%2C0%2C8v33c0%2C3.252%2C2.748%2C6%2C6%2C6h8v10c0%2C0.413%2C0.254%2C0.784%2C0.64%2C0.933C14.757%2C57.978%2C14.879%2C58%2C15%2C58c0.276%2C0%2C0.547-0.115%2C0.74-0.327L25.442%2C47H54c3.252%2C0%2C6-2.748%2C6-6V8C60%2C4.748%2C57.252%2C2%2C54%2C2z M58%2C41c0%2C2.168-1.832%2C4-4%2C4H27.179l3.579-4.161c0.36-0.418%2C0.313-1.05-0.105-1.41c-0.419-0.358-1.05-0.312-1.41%2C0.106l-4.982%2C5.792l0%2C0L16%2C54.414V46c0-0.552-0.447-1-1-1H6c-2.168%2C0-4-1.832-4-4V8c0-2.168%2C1.832-4%2C4-4h48c2.168%2C0%2C4%2C1.832%2C4%2C4V41z"%2F%3E%3C%2Fsvg%3E') 0 0/25px 20px no-repeat;width:25px;height:25px;display:inline-block;opacity:.6;vertical-align:middle;position:relative;top:3px} | |
.contents-full{display:none} | |
body.show-full-info .contents-simple{display:none} | |
body.show-full-info .contents-full{display:block} | |
.removal_banner{border:red solid 2px;padding:6px 20px 8px;border-radius:10px;background:#ff5151;color:white} | |
@media (prefers-color-scheme: dark) { | |
body {background:black;color:#DDD} | |
img {filter:brightness(0.8)} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="col s12"> | |
<!--<h6 class="removal_banner">bilibili漫画已更换链接授权规则,不同用户之间无法查看其他读者缓存的图片地址</h6>--> | |
<h3><?php echo $detail['title'];?></h3> | |
<div style="float:right"><?php echo implode('・', $detail['styles']);?></div> | |
<h5><?php echo implode(' ', $detail['author_name']);?></h5> | |
<?php | |
if ($detail['status'] !== 0) { | |
?> | |
<h6 class="removal_banner">该作品已下架</h6> | |
<?php | |
} | |
?> | |
<div style="float:right"><?php if (!empty($detail['square_cover'])) {?><img style="height:0;width:0" alt="封面" src="<?php echo wrapPicUrl(str_replace('http:','',$detail['square_cover']).'@100h.jpg');?>"><?php } ?><img style="height:200px" alt="封面" src="<?php echo wrapPicUrl(str_replace('http:','',$detail['vertical_cover']).'@400h.jpg');?>"><div style="text-align:center"><?php | |
$covers = []; | |
foreach (['horizontal_cover'=>'横封','vertical_cover'=>'竖封','square_cover'=>'方封'] as $key=>$name) { | |
if (!empty($detail[$key])) $covers[] = '<a href="'.wrapPicUrl(str_replace('http:','',$detail[$key])).'" target="_blank">'.$name.'</a>'; | |
} | |
echo implode(' ', $covers); | |
?></div></div> | |
<p style="white-space:pre-wrap"><?php echo $detail['evaluate'];?></p> | |
<p>上次阅读:<?php echo $detail['read_short_title'];?></p> | |
<p><?php echo $detail['is_finish'] ? '已完结' :$detail['renewal_time'];?></p> | |
<?php | |
if (!empty($discount_desc)) { | |
?> | |
<p>进行中的优惠:<?php echo implode(' & ', $discount_desc) ?> | <?php echo $detail['discount_end']?></p> | |
<?php | |
} | |
if (!empty($detail['disable_coupon_amount'])) { | |
?> | |
<p>最新 <?php echo $detail['disable_coupon_amount'] ?> 章不可使用福利券</p> | |
<?php | |
} | |
if (!empty($detail['wait_hour'])) { | |
?> | |
<p>包含等免章节,<?php echo $detail['wait_hour']?>小时一章节,<?php echo $detail['wait_free_at'] > date('Y-m-d H:i:s') ? $detail['wait_free_at'].' 可租赁下一章节' : '当前可租赁'?></p> | |
<?php | |
} | |
?> | |
<p><a href="/html/reply.htm#type=22&id=<?php echo $_GET['mangaid']?>&title=<?php echo rawurlencode($detail['title'])?>" target="_blank">评论区</a><span class="review"></span> | <a href="?act=batch_index&mangaid=<?php echo $_GET['mangaid']?>" target="_blank">刷新全部索引</a> | <a href="?act=batch_index&skip_cached&mangaid=<?php echo $_GET['mangaid']?>" target="_blank">获取未缓存索引</a> | <a href="?act=detail_preview&mangaid=<?php echo $_GET['mangaid']?>" target="_manga_view">章节预览</a></p> | |
<?php if (!empty($detail['album_count'])) { | |
?> | |
<p><a href="?act=album&mangaid=<?php echo $detail['id'];?>">特典解锁</a>(<?php echo $detail['album_count'] ?>章)</p> | |
<?php | |
}?> | |
<p> | |
<label style="color:inherit"><input type="checkbox" id="showFullChapterInfo" style="opacity:initial;position:initial;pointer-events:initial">显示完整目录</label> | |
<label style="color:inherit"><input type="checkbox" id="revertSort" autocomplete="off" style="opacity:initial;position:initial;pointer-events:initial">逆序目录</label> | |
<?php require 'sw_script.php'; ?> | |
</p> | |
<p>© Copyright <a href="https://manga.bilibili.com/m/detail/mc<?php echo $_GET['mangaid']?>" target="_blank">bilibili</a></p> | |
<div style="clear:both"></div> | |
<?php | |
if (!empty($detail['series_info']['comics'])) { | |
?> | |
<ul class="browser-default"> | |
<?php | |
foreach ($detail['series_info']['comics'] as $series_comic) { | |
?> | |
<li><?php echo $series_comic['comic_id'] == $detail['id'] ? $series_comic['title'] : '<a href="?act=detail&mangaid='.$series_comic['comic_id'].'">'.$series_comic['title'].'</a>'?></li> | |
<?php | |
} | |
?> | |
</ul> | |
<?php | |
} | |
?> | |
<div style="text-align:center" id="contents"><!-- | |
<?php | |
$idxCntStmt = $idxDb->prepare('SELECT count(hash) FROM index_data_history WHERE epid=?'); | |
$storedIdxChkStmt = $idxDb->prepare('SELECT count(id) FROM episode_index WHERE id=?'); | |
foreach ($detail['ep_list'] as $ep) { | |
$idxCntStmt->execute([$ep['id']]); | |
$idxCnt = $idxCntStmt->fetch()[0]; | |
$isRent = $ep['unlock_expire_at'] != '0000-00-00 00:00:00'; | |
$episodeClass = ['episode']; | |
if ($ep['is_locked'] && !$ep['is_in_free']) $episodeClass[] = 'locked'; | |
if ($idxCnt < 1) $episodeClass[] = 'no-index'; | |
if ($idxCnt > 1) $episodeClass[] = 'multiple-version'; | |
$storedIdxChkStmt->execute([$ep['id']]); | |
if ($storedIdxChkStmt->fetch()[0] > 0) { | |
$episodeClass = ['episode']; | |
} | |
?> | |
|--><span><!-- | |
|--><ruby class="contents-simple"><a class="<?php echo implode(' ', $episodeClass) ?> block" target="_manga_view" href="?act=read&mangaid=<?php echo $detail['id'];?>&epid=<?php echo $ep['id'];?>"><?php echo $ep['short_title'];?></a><rt>​<?php echo $ep['id'];?>​</rt></ruby><!-- | |
|--><div class="contents-full"><hr><a class="<?php echo implode(' ', $episodeClass) ?>" target="_manga_view" href="?act=read&mangaid=<?php echo $detail['id'];?>&epid=<?php echo $ep['id'];?>"><?php echo $ep['short_title'].'. '.$ep['title'];?></a><br><div class="flex hoz"><div class="epid">ID: <?php echo $ep['id'];?></div><div class="comments"><?php echo $ep['comments']?></div><div><?php echo ($ep['pay_gold'] ? $ep['is_locked'] || $isRent ? $ep['pay_gold'].' 漫币' . ($ep['allow_wait_free'] ? '/免费' : '') : '已购买' : '免费')?></div><div><?php echo $ep['read']?'已':'未'?>读过</div><div><?php echo implode(' / ', array_filter([prettySize($ep['size']), $ep['image_count'] . '页']))?></div></div><?php echo $ep['pub_time']?> 发布<?php if ($isRent) echo ' | 租赁至 '.$ep['unlock_expire_at'] ?></div><!-- | |
|--></span><!-- | |
<?php | |
} | |
?> | |
|--></div> | |
</div> | |
<script> | |
function review_count(data){if(data.code==0){[].slice.call(document.getElementsByClassName('review')).forEach(function(i){i.textContent='('+data.data.count+')';})}} | |
showFullChapterInfo.checked = localStorage.showFullChapterInfo == 1; | |
document.body.classList[showFullChapterInfo.checked?'add':'remove']('show-full-info'); | |
showFullChapterInfo.addEventListener('change', function () { | |
localStorage.showFullChapterInfo = this.checked ? 1 : 0; | |
document.body.classList[this.checked?'add':'remove']('show-full-info'); | |
}) | |
revertSort.checked = localStorage.revertSort == 1; | |
revertSort.addEventListener('change', function () { | |
localStorage.revertSort = this.checked ? 1 : 0; | |
for (var i = contents.children.length - 1; i>=0; i--) { | |
contents.appendChild(contents.children[i]); | |
} | |
}) | |
if (revertSort.checked) setTimeout(function () {revertSort.dispatchEvent(new Event('change'))}, 0); | |
if (Date.now() - parseInt(localStorage.oauthTime) > 24 * 60 * 60 * 1000 || localStorage.oauthTime == undefined) { | |
var s = document.createElement('script'); | |
s.src = '/login?act=expiretime&days=60'; | |
document.body.appendChild(s); | |
s.remove(); | |
} | |
</script> | |
<script src="https://www.biliplus.com/api/reply?isCount=1&oid=<?php echo $_GET['mangaid'];?>&type=22&jsonp=jsonp&callback=review_count&_=<?php echo time();?>" async></script> | |
</body> | |
</html> |
<?php | |
if (!ALLOW_SHARING) { | |
errordie('未开启缓存'); | |
} | |
if (empty($_GET['mangaid']) || !preg_match('(^\d+$)', $_GET['mangaid'])) { | |
errordie('无效id'); | |
} | |
chdir(__DIR__); | |
$idxDb = new PDO('sqlite:index.db'); | |
$mangaInfoStmt = $idxDb->prepare('SELECT * FROM manga_info WHERE id=?'); | |
$mangaInfoStmt->execute([$_GET['mangaid']]); | |
$mangaInfo = $mangaInfoStmt->fetch(PDO::FETCH_ASSOC); | |
if (empty($mangaInfo)) { | |
header('Location: ?act=detail&mangaid='.$_GET['mangaid'], true, 302); | |
exit; | |
} | |
$mangaTitle = $mangaInfo['title']; | |
$epList = json_decode(brotli_uncompress($mangaInfo['list']), true); | |
usort($epList, function ($a, $b) {return $a['ord'] > $b['ord'] ? 1 : -1;}); | |
$epLen = count($epList); | |
$usePaging = $epLen > 300; | |
if ($usePaging) { | |
$page = 1; | |
if (!empty($_GET['page'])) $page = ($_GET['page'] | 0) ?: 1; | |
$offset = ($page - 1) * 200; | |
if ($offset > $epLen) $offset = 0; | |
$len = 200; | |
if ($epLen - $offset <= 250) { | |
$len = $epLen - $offset; | |
} | |
$epList = array_slice($epList, $offset, $len); | |
$epPages = ceil($epLen / 200) | 0; | |
if (($epLen % 200) <= 50) $epPages--; | |
} | |
$imgUrlRequestParam = [ | |
'act' => 'get_image_url', | |
'request_time' => time(), | |
]; | |
$epIdxStmt = $idxDb->prepare('SELECT b.data,b.uid FROM index_data AS a,index_data_history AS b WHERE a.epid=? AND a.epid=b.epid AND a.hash=b.hash'); | |
$imgUrl = []; | |
foreach ($epList as &$ep) { | |
$epIdxStmt->execute([$ep['id']]); | |
$epIdx = $epIdxStmt->fetch(); | |
if (empty($epIdx)) continue; | |
$uid = $epIdx['uid']; | |
$epIdx = json_decode(brotli_uncompress($epIdx['data']), true); | |
$idx = 0; | |
if (in_array(pathinfo($epIdx[$idx]['path'], PATHINFO_BASENAME), ['dc7914d65771003337d24be6281c6189934b89c0.jpg', 'e06419ed685fab1df07134fc4e0f2e9011808cb5.jpg'])) $idx++; | |
$img = getImgUrl($epIdx, $idx, 150, $ep['id']); | |
$img['path'] = preg_replace('(.*//[^/]+(.+))', '$1', $img['url']); | |
$ep['first_image'] = $img; | |
if (!isset($imgUrl[$uid])) $imgUrl[$uid] = []; | |
$imgUrl[$uid][] = $img['url']; | |
} | |
unset($ep); | |
function getImgUrl(&$indexData, $i, $setWidth, $epid) { | |
global $imgUrlRequestParam; | |
$imgUrlRequestParam['epid'] = $epid; | |
$append = '@'.($setWidth*2).'w.jpg'; | |
if ($indexData[$i]['x'] <= $setWidth * 2) { | |
$append = '@.jpg'; | |
} | |
$imgUrlRequestParam['file'] = pathinfo($indexData[$i]['path'], PATHINFO_BASENAME); | |
$imgUrlRequestParam['append'] = $append; | |
return [ | |
'url' => '?' . http_build_query($imgUrlRequestParam), | |
'size' => [ | |
$setWidth, | |
$setWidth / $indexData[$i]['x'] * $indexData[$i]['y'] | |
] | |
]; | |
} | |
?> | |
<!DOCTYPE html><html> | |
<head> | |
<meta charset="UTF-8" name="viewport" content="width=device-width"> | |
<meta name="format-detection" content="telephone=no" /> | |
<meta name="referrer" content="never"> | |
<title>章节预览 - <?php echo $mangaInfo['title'];?> - 书籍 - bilibili漫画 阅读器 - BiliPlus</title> | |
<script src="materialize.min.js"></script> | |
<link rel="stylesheet" href="materialize.min.css" /> | |
<style> | |
body {width:95%;max-width:700px;margin:5px auto;background:#EFEFF4;cursor:default} | |
.episode-item{margin:5px 3px;display:inline-block} | |
.episode-item img{width:150px} | |
rt{font-size:100%;color:#888} | |
.ep-title{width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} | |
@media (prefers-color-scheme: dark) { | |
body {background:black;color:#DDD} | |
img {filter:brightness(0.8)} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="col s12"> | |
<h3><?php echo $mangaInfo['title'];?></h3> | |
<p>© Copyright <a href="https://manga.bilibili.com/m/detail/mc<?php echo $_GET['mangaid']?>" target="_blank">bilibili</a></p> | |
<div style="clear:both"></div> | |
<?php | |
function printPaging() { | |
global $usePaging, $offset, $epPages, $epLen, $epList; | |
if ($usePaging) { | |
$page = $offset / 200 + 1; | |
$get = $_GET; | |
?> | |
<hr> | |
<center><p><?php printf("%s-%s/%s", $offset + 1, $offset + count($epList), $epLen);?></p></center> | |
<center><ul class="pagination"> | |
<li class="<?php echo $page===1 ? 'active' : 'waves-effect'?>"><a href="?<?php $get['page']=1;echo http_build_query($get);?>">1</a></li> | |
<?php | |
if ($page > 4) { | |
?> | |
<li class="disabled">…</li> | |
<?php } | |
for ($i = 0; $i<5; $i++) { | |
$p = $page - 2 + $i; | |
if ($p > $epPages - 1) break; | |
if ($p < 2) continue; | |
?> | |
<li class="<?php echo $p===$page ? 'active' : 'waves-effect'?>"><a href="?<?php $get['page']=$p;echo http_build_query($get);?>"><?php echo $p?></a></li> | |
<?php } | |
if ($page < $epPages - 3) { | |
?> | |
<li class="disabled">…</li> | |
<?php } ?> | |
<li class="<?php echo $page===$epPages ? 'active' : 'waves-effect'?>"><a href="?<?php $get['page']=$epPages;echo http_build_query($get);?>"><?php echo $epPages?></a></li> | |
</ul></center> | |
<?php | |
} | |
} | |
printPaging(); | |
?> | |
<div style="text-align:center"><!-- | |
<?php | |
foreach ($epList as $ep) { | |
$attr = 'src="about:blank"'; | |
if (isset($ep['first_image'])) { | |
$attr = '_src="'.$ep['first_image']['url'].'" width="'.$ep['first_image']['size'][0].'" height="'.$ep['first_image']['size'][1].'"'; | |
} | |
?> | |
|--><div class="episode-item"><a href="?act=read&mangaid=<?php echo $mangaInfo['id'];?>&epid=<?php echo $ep['id'];?>"><img <?php echo $attr?>></a><br><?php echo $ep['short_title'].' ('.$ep['id'];?>)<?php if (!empty($ep['title'])) echo '<br><div class="ep-title">'.$ep['title'].'</div>'?></div><!-- | |
<?php | |
} | |
?> | |
|--></div> | |
<?php printPaging() ?> | |
</div> | |
<script> | |
window.img_lazyload=function(){var t=[].slice.call(document.getElementsByTagName("img"));t.forEach(function(t){if(t.hasAttribute("_src")){var e=t.getBoundingClientRect();e.bottom<-100||e.top>innerHeight+1000||e.right<-1000||e.left>innerWidth+100||(t.setAttribute("src",t.getAttribute("_src")),t.removeAttribute("_src"))}})},window.addEventListener("scroll",img_lazyload),window.addEventListener("resize",img_lazyload); | |
img_lazyload(); | |
</script> | |
</body> | |
</html> |
<?php | |
if (empty($_GET['epid']) || !preg_match('(^\d+$)', $_GET['epid'])) { | |
errordie('无效id'); | |
} | |
chdir(__DIR__); | |
$idxDb = new PDO('sqlite:index.db'); | |
$result = $idxDb->prepare('SELECT * FROM index_data_history WHERE epid=?'); | |
$result->execute([$_GET['epid']]); | |
$resultArr = array_map(function ($i) {$i['data'] = json_decode(brotli_uncompress($i['data']), true);return $i;}, $result->fetchAll(PDO::FETCH_ASSOC)); | |
usort($resultArr, function ($a, $b) {return $a['time'] - $b['time'];}); | |
$diff = []; | |
$imgInfo = []; | |
for ($i=0; $i<count($resultArr) - 1; $i++) { | |
$subdiff = []; | |
$len = max(count($resultArr[$i]['data']['pics']), count($resultArr[$i+1]['data']['pics'])); | |
for ($j=0; $j<$len; $j++) { | |
if ($resultArr[$i]['data']['pics'][$j] != $resultArr[$i+1]['data']['pics'][$j]) { | |
$imgInfo[ $resultArr[$i]['data']['pics'][$j] ] = $resultArr[$i]['data']['sizes'][$j]; | |
$imgInfo[ $resultArr[$i+1]['data']['pics'][$j] ] = $resultArr[$i+1]['data']['sizes'][$j]; | |
$subdiff[] = [ | |
$j + 1, | |
$resultArr[$i]['data']['pics'][$j], | |
$resultArr[$i+1]['data']['pics'][$j] | |
]; | |
} | |
} | |
$diff[] = [ | |
'hash' => [dechex($resultArr[$i]['hash']), dechex($resultArr[$i+1]['hash'])], | |
'time' => [$resultArr[$i]['time'], $resultArr[$i+1]['time']], | |
'diff' => $subdiff | |
]; | |
} | |
unset($imgInfo['']); | |
$setWidth = 400; | |
foreach (array_keys($imgInfo) as $img) { | |
$size = $imgInfo[$img]; | |
$append = '@'.($setWidth*2).'w.jpg'; | |
if ($size['cx'] <= $setWidth * 2) { | |
$append = '@.jpg'; | |
} | |
$url = 'https://'.IMG_HOST.$img.$append; | |
$imgInfo[$img] = [ | |
'cx' => $setWidth, | |
'cy' => $setWidth / $size['cx'] * $size['cy'], | |
'url' => $url | |
]; | |
} | |
$imgUrl = array_values(array_map(function ($i){return $i['url'];}, $imgInfo)); | |
$tokens = empty($imgUrl) ? ['code'=>0,'data'=>[]]: json_decode(preg_replace('/https?:\/\/i(0|1|2|s)\.hdslb\.com\//', 'https://'.IMG_HOST.'/', cget('http://manga.bilibili.com/twirp/comic.v1.Comic/ImageToken', [ | |
'post' => buildParam([ | |
'access_key'=>$_COOKIE['access_key'], | |
'actionKey'=>'appkey', | |
'appkey'=>$appkey, | |
'build'=>APPBUILD, | |
'device'=>'phone', | |
'mobi_app'=>'iphone_comic', | |
'platform'=>'ios', | |
'ts'=>time(), | |
'urls'=>json_encode($imgUrl), | |
'version'=>APPVER, | |
], $appsecret) | |
])), true); | |
if (!$tokens || $tokens['code'] != 0) { | |
errordie('获取凭据出错: ['.$tokens['code'].']'.$tokens['msg']); | |
} | |
$tokenMap = []; | |
foreach ($tokens['data'] as $item) { | |
$tokenMap[$item['url']] = $item['token']; | |
} | |
?> | |
<!DOCTYPE html><html> | |
<head> | |
<meta charset="UTF-8" name="viewport" content="width=device-width"> | |
<meta name="format-detection" content="telephone=no" /> | |
<meta name="referrer" content="never"> | |
<title>章节索引历史 - bilibili漫画 阅读器 - BiliPlus</title> | |
<script src="materialize.min.js"></script> | |
<link rel="stylesheet" href="materialize.min.css" /> | |
<style> | |
body {width:800px;margin:5px auto;background:#EFEFF4;cursor:default;text-align:center} | |
span.col {display:inline-block} | |
.row img.col{padding:0} | |
@media (prefers-color-scheme: dark) { | |
body {background:black;color:#DDD} | |
img {filter:brightness(0.8)} | |
} | |
.invert-diff p+img.col.s6 { | |
filter: none; | |
position: relative; | |
left: 25% | |
} | |
.invert-diff p+img+img.col.s6 { | |
position: relative; | |
left: -25%; | |
opacity: 0.5; | |
filter: invert(100%); | |
} | |
</style> | |
</head> | |
<body> | |
<div class="row s12"> | |
<?php | |
foreach ($diff as $subdiff) { | |
?> | |
<hr> | |
<span class="col s6"><?php echo date('Y/m/d H:i:s', $subdiff['time'][0])?><br><?php echo $subdiff['hash'][0]?></span> | |
<span class="col s6"><?php echo date('Y/m/d H:i:s', $subdiff['time'][1])?><br><?php echo $subdiff['hash'][1]?></span> | |
<?php | |
foreach ($subdiff['diff'] as $diffitem) { | |
?> | |
<p>P<?php echo $diffitem[0]?></p> | |
<img class="col s6" width="<?php echo $imgInfo[$diffitem[1]]['cx']?>" height="<?php echo $imgInfo[$diffitem[1]]['cy']?>" _src="<?php $url = $imgInfo[$diffitem[1]]['url']; echo $url.'?token='.$tokenMap[$url]?>"><!-- | |
|--><img class="col s6" width="<?php echo $imgInfo[$diffitem[2]]['cx']?>" height="<?php echo $imgInfo[$diffitem[2]]['cy']?>" _src="<?php $url = $imgInfo[$diffitem[2]]['url']; echo $url.'?token='.$tokenMap[$url]?>"> | |
<?php | |
} | |
} | |
?> | |
</div> | |
<div class="fixed-action-btn" id="invert_toggle"><div style="background:rgba(255,255,255,.6);text-align:center;font-family:Arial;color:#000;padding:2px 8px">切换<br>负片</div></div> | |
<script> | |
window.img_lazyload=function(){var t=[].slice.call(document.getElementsByTagName("img"));t.forEach(function(t){if(t.hasAttribute("_src")){var e=t.getBoundingClientRect();e.bottom<-100||e.top>innerHeight+1000||e.right<-1000||e.left>innerWidth+100||(t.setAttribute("src",t.getAttribute("_src")),t.removeAttribute("_src"))}})},window.addEventListener("scroll",img_lazyload),window.addEventListener("resize",img_lazyload),img_lazyload(); | |
invert_toggle.addEventListener('click', function () { | |
document.body.classList.toggle('invert-diff') | |
}) | |
</script> | |
</body> | |
</html> |
<?php | |
define('BILI_API_USE_USER_IP', true); | |
define('HMAC_KEY', file_get_contents(__DIR__.'/.hmac_key.txt')); | |
function get_url_error($errorString) { | |
if (isset($_SERVER['HTTP_X_FETCH_REQUEST'])) { | |
header('Content-Type: application/json'); | |
echo json_encode(['success' => false, 'error' => $errorString], JSON_UNESCAPED_UNICODE+JSON_UNESCAPED_SLASHES); | |
} else { | |
// svg with text | |
header('Content-Type: image/svg+xml'); | |
$errorString = implode('', array_map(function ($s) { | |
return '<tspan x="0" dy="1.2em">'.$s.'</tspan>'; | |
}, explode("\n", $errorString))); | |
echo '<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg width="700" height="700" version="1.1" xmlns="http://www.w3.org/2000/svg"><text x="20" y="0" font-size="60" text-anchor="start" fill="black" stroke="white" stroke-width="2" font-weight="bold">'.$errorString.'</text></svg>'; | |
} | |
exit; | |
} | |
if (empty($_GET['epid'])) { | |
get_url_error("参数错误"); | |
} | |
if (empty($_GET['file']) && empty($_GET['path'])) { | |
get_url_error('参数错误'); | |
} | |
chdir(__DIR__); | |
$idxDb = new PDO('sqlite:index.db'); | |
if (!empty($_GET['path'])) { | |
$urls = [$_GET['path']]; | |
$usingAccessKey = $_COOKIE['access_key']; | |
} else { | |
$epid = $_GET['epid']; | |
$file = $_GET['file']; | |
$append = ''; | |
if (!empty($_GET['append'])) { | |
$append = $_GET['append']; | |
} | |
$storedImageStmt = $idxDb->prepare('SELECT remote_path,offset,length FROM stored_image WHERE path=?'); | |
$storedImageStmt->execute([$file]); | |
$row = $storedImageStmt->fetch(); | |
if (!empty($row)) { | |
$url = implode('/', [time(), $row['offset'] .'-'. ($row['offset'] + $row['length']), $row['remote_path']]); | |
$sign = hash_hmac('sha1', $url, HMAC_KEY); | |
$imgUrl = 'https://9bb7278df77e39b1.biliplus.com/'.$sign.'/'.$url; | |
if (isset($_SERVER['HTTP_X_FETCH_REQUEST'])) { | |
header('Content-Type: application/json'); | |
echo json_encode(['success' => true, 'url' => $imgUrl], JSON_UNESCAPED_UNICODE+JSON_UNESCAPED_SLASHES); | |
exit; | |
} else { | |
header('Cache-Control: max-age=600, public, immutable'); | |
header('Location: '.$imgUrl, true, 301); | |
header('Content-Length: 0'); | |
exit; | |
} | |
} | |
$stmt = $idxDb->prepare('SELECT b.data AS `data`,a.hash,b.uid FROM index_data AS a,index_data_history AS b WHERE a.epid=? AND a.epid=b.epid AND a.hash=b.hash'); | |
$stmt->execute([$epid]); | |
$row = $stmt->fetch(); | |
if (!empty($row)) { | |
$indexData = json_decode(brotli_uncompress($row['data']), true); | |
$hash = $row['hash']; | |
$uid = $row['uid']; | |
$stmt = $idxDb->prepare('SELECT accesskey,ip FROM account WHERE uid=?'); | |
$stmt->execute([$uid]); | |
$row = $stmt->fetch(); | |
if (!empty($row)) { | |
$usingAccessKey = $row['accesskey']; | |
$_COOKIE['user_ip_enc'] = $row['ip']; | |
} | |
} else { | |
get_url_error('未找到索引'); | |
} | |
$urls = []; | |
foreach ($indexData as $img) { | |
if (pathinfo($img['path'], PATHINFO_BASENAME) === $file) { | |
$urls[] = 'https://'.IMG_HOST.$img['path'].$append; | |
break; | |
} | |
} | |
if (empty($urls)) { | |
get_url_error('未找到图片'); | |
} | |
} | |
$tokens = json_decode(cget('http://manga.bilibili.com/twirp/comic.v1.Comic/ImageToken', [ | |
'post' => buildParam([ | |
'access_key'=>$usingAccessKey, | |
'actionKey'=>'appkey', | |
'appkey'=>$appkey, | |
'build'=>APPBUILD, | |
'device'=>'phone', | |
'mobi_app'=>'iphone_comic', | |
'platform'=>'ios', | |
'ts'=>time(), | |
'urls'=>json_encode($urls), | |
'version'=>APPVER, | |
], $appsecret) | |
]), true); | |
if (!$tokens || $tokens['code'] !== 0) { | |
if (isset($uid) && $tokens && $tokens['code'] === 'invalid_argument' && in_array(substr($tokens['msg'], 0, 9), ['bucket.u ', 'bucket.b '])) { | |
// accesskey无效,删除该用户所有缓存 | |
// 240705: bucket.b = 用户封禁 | |
$moveStmt = $idxDb->prepare('INSERT INTO index_data_expired (epid,mangaid,hash,data,time,uid) SELECT * FROM index_data_history WHERE uid=?'); | |
$deleteStmt = $idxDb->prepare('DELETE FROM index_data_history WHERE uid=?'); | |
$expireLogStmt = $idxDb->prepare('INSERT INTO expire_log (uid,log_time,deleted_chapters) VALUES (?,?,?)'); | |
$accountKeyDelete = $idxDb->prepare('UPDATE account SET keyexpire=0 WHERE uid=?'); | |
$moveStmt->execute([$uid]); | |
$moveStmt->closeCursor(); | |
$deleteStmt->execute([$uid]); | |
$deleteCount = $idxDb->query('SELECT changes()')->fetch()[0]; | |
$deleteStmt->closeCursor(); | |
$expireLogStmt->execute([$uid, time(), $deleteCount]); | |
$expireLogStmt->closeCursor(); | |
$accountKeyDelete->execute([$uid]); | |
$accountKeyDelete->closeCursor(); | |
} | |
get_url_error('获取凭据出错: ['.$tokens['code']."]\n".$tokens['msg']); | |
} | |
$imgUrl = wrapPicUrl($tokens['data'][0]['url'].'?token='.$tokens['data'][0]['token'].'&no_cache=1'); | |
if (isset($_SERVER['HTTP_X_FETCH_REQUEST'])) { | |
header('Content-Type: application/json'); | |
echo json_encode(['success' => true, 'url' => $imgUrl], JSON_UNESCAPED_UNICODE+JSON_UNESCAPED_SLASHES); | |
exit; | |
} else { | |
header('Cache-Control: max-age=600, public, immutable'); | |
header('Location: '.$imgUrl, true, 301); | |
header('Content-Length: 0'); | |
exit; | |
} |
<?php | |
require_once '../include/gzip.php'; | |
require_once $root_prefix.'include/functions.php'; | |
$appkey = 'da44a5d9227fa9ef'; | |
$appsecret = 'ad875eed760f65ac5ade5f363ab05e42'; | |
define('APPVER', '6.8.5'); | |
define('APPBUILD', '2175'); | |
define('IMG_HOST', 'manga.hdslb.com'); | |
define('USE_BILI_STATIC', false); | |
header('NO-CONVERT-IMG: 1'); | |
header('Content-Type: text/html'); | |
define('OVERSEA_MANGA_IDS', []); | |
//errordie('维护中'); | |
if (isset($_GET['set_buy_platform'])) { | |
if (in_array($_GET['set_buy_platform'], ['ios', 'android'])) { | |
setcookie('manga-buy-platform', $_GET['set_buy_platform'], 0x7fffffff, '/manga/', $domain, true, true); | |
} | |
$uri = '/manga/'; | |
unset($_GET['set_buy_platform']); | |
if (!empty($_GET)) { | |
$uri .= '?'.http_build_query($_GET); | |
} | |
header('Location: '.$uri, true, 302); | |
exit; | |
} | |
$platform = 'ios'; | |
if (!empty($_COOKIE['manga-buy-platform']) && $_COOKIE['manga-buy-platform'] == 'android') $platform = 'android'; | |
define('PLATFORM', $platform); | |
function errordie($reason, $extraHead = '') { | |
?> | |
<!DOCTYPE html><html> | |
<head> | |
<meta charset="UTF-8" name="viewport" content="width=device-width,user-scalable=no"> | |
<meta name="format-detection" content="telephone=no" /> | |
<?php echo $extraHead?> | |
<title>bilibili漫画 阅读器 - BiliPlus</title> | |
<style> | |
body {width:95%;max-width:600px;margin:5px auto !important;background:#EFEFF4;cursor:default;word-break:break-all} | |
@media (prefers-color-scheme: dark) { | |
body {background:black;color:#DDD} | |
img {filter:brightness(0.8)} | |
} | |
</style> | |
</head> | |
<body> | |
<p><?php echo $reason;?></p> | |
</body> | |
</html><?php | |
exit; | |
} | |
function wrapPicUrl(string $url) { | |
if (!USE_BILI_STATIC) return $url; | |
if (!preg_match('/^.*\/\/([^\.]+\.hdslb\.com.*)$/', $url, $match)) return $url; | |
return 'https://bili-static.acgvideo.com/'.urlencode($match[1]); | |
} | |
if ($_COOKIE['login'] != 2 || empty($_COOKIE['mid']) || empty($_COOKIE['access_key']) || empty($_COOKIE['expires'])) { | |
errordie('未登录<br><a href="javascript:localStorage.enablePlayback=\'on\',location.href=\'/login\'">登录</a>'); | |
} | |
require_once 'sharing_agreement.php'; | |
if (isset($_GET['act'])) { | |
switch ($_GET['act']) { | |
case 'detail': { | |
require_once 'detail.php'; | |
exit; | |
} | |
case 'batch_index': { | |
require_once 'batch_index.php'; | |
exit; | |
} | |
case 'detail_preview': { | |
require_once 'detail_preview.php'; | |
exit; | |
} | |
case 'read': { | |
require_once 'read.php'; | |
exit; | |
} | |
case 'diff': { | |
errordie('维护中'); | |
require_once 'diff.php'; | |
exit; | |
} | |
case 'album': { | |
errordie('维护中'); | |
require_once 'album.php'; | |
exit; | |
} | |
case 'read_album': { | |
errordie('维护中'); | |
require_once 'read_album.php'; | |
exit; | |
} | |
case 'show_agreement': { | |
showAgreementPage(); | |
exit; | |
} | |
case 'list_purchased': { | |
require_once 'list_purchased.php'; | |
exit; | |
} | |
case 'get_image_url': { | |
require_once 'get_image_url.php'; | |
exit; | |
} | |
} | |
} | |
require_once 'listfav.php'; |
<?php | |
define('BILI_API_USE_USER_IP', true); | |
$page = 1; | |
if (!empty($_GET['page']) && preg_match('/^\d+$/', $_GET['page'])) { | |
$page = $_GET['page']; | |
} | |
if (isset($_GET['order'])) { | |
if (in_array($_GET['order'], ['add','last_update','last_read'])) { | |
setcookie('manga-order', $_GET['order'], 0x7fffffff, '/manga/', $domain, true, true); | |
} | |
$uri = '/manga/'; | |
unset($_GET['order']); | |
if (!empty($_GET)) { | |
$uri .= '?'.http_build_query($_GET); | |
} | |
header('Location: '.$uri, true, 302); | |
exit; | |
} | |
$pagesize = 36; | |
$curl = curl_init(); | |
chdir(__DIR__); | |
$idxDb = new PDO('sqlite:index.db'); | |
$mangaInfoStmt = $idxDb->prepare('SELECT list FROM manga_info WHERE id=?'); | |
function getEpPrice($mangaId) { | |
global $mangaInfoStmt; | |
$mangaInfoStmt->execute([$mangaId]); | |
$mangaInfo = $mangaInfoStmt->fetch(PDO::FETCH_ASSOC); | |
if (empty($mangaInfo)) return 0; | |
$epList = json_decode(brotli_uncompress($mangaInfo['list']), true); | |
$count = []; | |
foreach ($epList as $ep) { | |
$price = $ep['pay_gold']; | |
if ($price <= 0) continue; | |
if (empty($count[$price])) $count[$price] = 0; | |
$count[$price]++; | |
} | |
$commonPrice = 0; | |
$commonCount = 0; | |
foreach ($count as $price=>$c) { | |
if ($c > $commonCount) { | |
$commonPrice = $price; | |
$commonCount = $c; | |
} | |
} | |
return $commonPrice; | |
} | |
$purchasedList = cget('http://manga.bilibili.com/twirp/user.v1.User/GetAutoBuyComics?', [ | |
'curl' => $curl, | |
'post' => buildParam([ | |
'access_key'=>$_COOKIE['access_key'], | |
'actionKey'=>'appkey', | |
'appkey'=>$appkey, | |
'build'=>APPBUILD, | |
'device'=>'phone', | |
'mobi_app'=>'iphone_comic', | |
'platform'=>'ios', | |
'ts'=>time(), | |
'version'=>APPVER, | |
'page_num'=>$page, | |
'page_size'=>$pagesize, | |
'type'=>0, | |
], $appsecret) | |
]); | |
if (isset($_GET['api'])) { | |
header('Content-Type: application/json; charset="UTF-8"'); | |
echo $purchasedList; exit; | |
} | |
$purchasedList = json_decode($purchasedList, true); | |
$hasPrevPage = $page > 1; | |
$hasNextPage = !empty($purchasedList['data']); | |
?> | |
<!DOCTYPE html><html> | |
<head> | |
<meta charset="UTF-8" name="viewport" content="width=device-width,user-scalable=no"> | |
<meta name="format-detection" content="telephone=no" /> | |
<meta name="referrer" content="never"> | |
<title>已购漫画 - bilibili漫画 阅读器 - BiliPlus</title> | |
<script src="materialize.min.js" defer></script> | |
<script> | |
document.addEventListener('DOMContentLoaded', function () { | |
var elems = document.querySelectorAll('select'); | |
var instances = M.FormSelect.init(elems, {}); | |
if (Date.now() - parseInt(localStorage.oauthTime) > 24 * 60 * 60 * 1000 || localStorage.oauthTime == undefined) { | |
var s = document.createElement('script'); | |
s.src = '/login?act=expiretime&days=60'; | |
document.body.appendChild(s); | |
s.remove(); | |
} | |
}); | |
</script> | |
<link rel="stylesheet" href="materialize.min.css" /> | |
<style> | |
body {width:95%;max-width:600px;margin:5px auto;background:#EFEFF4;cursor:default} | |
.comic-cover{height:200px} | |
.disabled{pointer-events:none} | |
.removal_banner{border:red solid 2px;padding:6px 20px 8px;border-radius:10px;background:#ff5151;color:white} | |
@media (prefers-color-scheme: dark) { | |
body {background:black;color:#DDD} | |
.card,.dropdown-content {background:#1c1c1c} | |
.select-wrapper input.select-dropdown {color:white} | |
.select-wrapper .caret {fill:rgba(255,255,255,0.87)} | |
.pagination li a {color:white} | |
img {filter:brightness(0.8)} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="col s12"> | |
<h2>已购漫画</h2> | |
<p class="row"> | |
<span class="col s6">© Copyright <a href="https://manga.bilibili.com/m/" target="_blank">bilibili</a></span> | |
</p> | |
<div> | |
<?php require 'sw_script.php'; ?> | |
</div> | |
<?php | |
if ($hasPrevPage || $hasNextPage) { | |
?> | |
<ul class="pagination row s12"> | |
<li class="col s2 waves-effect<?php if (!$hasPrevPage) echo ' disabled'; ?>"><a href="?act=list_purchased&page=<?php echo $page-1;?>"><</a></li> | |
<li class="col s8"></li> | |
<li class="col s2 waves-effect<?php if (!$hasNextPage) echo ' disabled'; ?>"><a href="?act=list_purchased&page=<?php echo $page+1;?>">></a></li> | |
</ul> | |
<?php | |
} | |
foreach ($purchasedList['data'] as $item) { | |
?> | |
<div class="card horizontal"> | |
<div class="card-image"> | |
<img class="comic-cover" alt="封面" src="<?php echo wrapPicUrl(str_replace('http:','',$item['vcover']).'@400h.jpg');?>"> | |
</div> | |
<div class="card-stacked"> | |
<div class="card-content"> | |
<p><?php echo $item['comic_title'];?></p> | |
<?php | |
$epPrice = getEpPrice($item['comic_id']); | |
if ($epPrice > 0) { | |
?> | |
<p>单章价格:<?php echo $epPrice;?> 漫币</p> | |
<?php | |
} | |
?> | |
<p>最近更新:[<?php echo $item['last_short_title'];?>]</p> | |
<p>已购买 <?php echo $item['bought_ep_count'];?> 章节</p> | |
<?php if ($item['comic_status'] != 0) { ?> | |
<p style="color:red">该作品已下架</p> | |
<?php } ?> | |
</div> | |
<div class="card-action"> | |
<a href="?act=detail&mangaid=<?php echo $item['comic_id'];?>">查看目录</a> | |
</div> | |
</div> | |
</div> | |
<?php | |
} | |
if (empty($purchasedList['data'])) { | |
?> | |
<p style="text-align:center">没有了</p> | |
<?php | |
} | |
if (count ($purchasedList['data']) > 3 && ($hasPrevPage || $hasNextPage)) { | |
?> | |
<ul class="pagination row s12"> | |
<li class="col s2 waves-effect<?php if (!$hasPrevPage) echo ' disabled'; ?>"><a href="?act=list_purchased&page=<?php echo $page-1;?>"><</a></li> | |
<li class="col s8"></li> | |
<li class="col s2 waves-effect<?php if (!$hasNextPage) echo ' disabled'; ?>"><a href="?act=list_purchased&page=<?php echo $page+1;?>">></a></li> | |
</ul> | |
<?php | |
} | |
?> | |
</div> | |
</body> | |
</html> |
<?php | |
define('BILI_API_USE_USER_IP', true); | |
$page = 1; | |
if (!empty($_GET['page']) && preg_match('/^\d+$/', $_GET['page'])) { | |
$page = $_GET['page']; | |
} | |
if (isset($_GET['order'])) { | |
if (in_array($_GET['order'], ['add','last_update','last_read'])) { | |
setcookie('manga-order', $_GET['order'], 0x7fffffff, '/manga/', $domain, true, true); | |
} | |
$uri = '/manga/'; | |
unset($_GET['order']); | |
if (!empty($_GET)) { | |
$uri .= '?'.http_build_query($_GET); | |
} | |
header('Location: '.$uri, true, 302); | |
exit; | |
} | |
$pagesize = 36; | |
$order = 2; | |
if (!empty($_COOKIE['manga-order'])) { | |
switch ($_COOKIE['manga-order']) { | |
case 'add': { | |
$order = 1; | |
break; | |
} | |
case 'last_read': { | |
$order = 3; | |
} | |
} | |
} | |
$curl = curl_init(); | |
chdir(__DIR__); | |
$idxDb = new PDO('sqlite:index.db'); | |
$mangaInfoStmt = $idxDb->prepare('SELECT list FROM manga_info WHERE id=?'); | |
function getEpPrice($mangaId) { | |
global $mangaInfoStmt; | |
$mangaInfoStmt->execute([$mangaId]); | |
$mangaInfo = $mangaInfoStmt->fetch(PDO::FETCH_ASSOC); | |
if (empty($mangaInfo)) return 0; | |
$epList = json_decode(brotli_uncompress($mangaInfo['list']), true); | |
$count = []; | |
foreach ($epList as $ep) { | |
$price = $ep['pay_gold']; | |
if ($price <= 0) continue; | |
if (empty($count[$price])) $count[$price] = 0; | |
$count[$price]++; | |
} | |
$commonPrice = 0; | |
$commonCount = 0; | |
foreach ($count as $price=>$c) { | |
if ($c > $commonCount) { | |
$commonPrice = $price; | |
$commonCount = $c; | |
} | |
} | |
return $commonPrice; | |
} | |
if (!empty($_COOKIE['vipDueDate']) && (empty($_COOKIE['vip-monthly-coupons']) || $_COOKIE['vip-monthly-coupons'] < time())) { | |
$getVipReward = json_decode(cget('https://manga.bilibili.com/twirp/user.v1.User/GetVipReward', [ | |
'curl' => $curl, | |
'post' => buildParam([ | |
'access_key'=>$_COOKIE['access_key'], | |
'actionKey'=>'appkey', | |
'appkey'=>$appkey, | |
'build'=>APPBUILD, | |
'device'=>'phone', | |
'mobi_app'=>'iphone_comic', | |
'order'=>$order, | |
'page_num'=>$page, | |
'page_size'=>$pagesize, | |
'platform'=>'ios', | |
'ts'=>time(), | |
'version'=>APPVER, | |
'reason_id'=>1, | |
], $appsecret) | |
]), true); | |
if (!empty($getVipReward)) { | |
if ($getVipReward['code'] == 1 || $getVipReward['code'] === 0) { | |
// start from 1646064000 Mar 01 2022 00:00:00 GMT+0800 | |
$nextReward = ceil((time()-1646064000)/2678400)*2678400+1646064000; | |
setcookie('vip-monthly-coupons', $nextReward, 0x7fffffff, '/manga/', $domain, true, true); | |
} | |
} | |
} | |
$accessRestricted = false; | |
if (empty($_COOKIE['manga-init-checked'])) { | |
setcookie('manga-init-checked', '1', time() + 30*60, '/manga/', $domain, false, false); | |
$initResponse = json_decode(cget('https://manga.bilibili.com/twirp/user.v1.User/GetInitInfo', [ | |
'curl' => $curl, | |
'post' => buildParam([ | |
'access_key'=>$_COOKIE['access_key'], | |
'actionKey'=>'appkey', | |
'appkey'=>$appkey, | |
'build'=>APPBUILD, | |
'device'=>'phone', | |
'mobi_app'=>'iphone_comic', | |
'platform'=>'ios', | |
'ts'=>time(), | |
'version'=>APPVER, | |
], $appsecret) | |
]), true); | |
if (!empty($initResponse['data']['need_sms_verify'])) { | |
$accessRestricted = true; | |
setcookie('manga-access-restricted', '1', time() + 30*60, '/manga/', $domain, false, false); | |
} | |
} | |
if (!empty($_COOKIE['manga-access-restricted'])) { | |
$accessRestricted = true; | |
} | |
$favList = cget('http://manga.bilibili.com/twirp/bookshelf.v1.Bookshelf/ListFavorite?'.buildParam([ | |
'appkey'=>'da44a5d9227fa9ef', | |
'mobi_app'=>'iphone_comic', | |
'version'=>APPVER, | |
'build'=>APPBUILD, | |
'channel'=>'AppStore', | |
'platform'=>'ios', | |
'device'=>'phone', | |
'access_key'=>$_COOKIE['access_key'], | |
'is_teenager'=>'0', | |
'no_recommend'=>'0', | |
'network'=>'wifi', | |
'ts'=>time(), | |
], $appsecret), [ | |
'curl' => $curl, | |
'header' => [ | |
'User-Agent' => 'comic-universal/2140 CFNetwork/1240.0.4 Darwin/20.6.0 os/ios model/iPhone 15 mobi_app/iphone_comic build/2140 osVer/14.8 network/2 channel/AppStore', | |
'Content-Type'=> 'application/json; charset=utf-8', | |
], | |
'post' => json_encode([ | |
'groupId'=>0, | |
'order'=>$order, | |
'pageNum'=>$page, | |
'pageSize'=>$pagesize, | |
'timeLimitFree'=>0, | |
'type'=>0, | |
'waitFree'=>0, | |
], JSON_UNESCAPED_SLASHES+JSON_UNESCAPED_UNICODE), | |
]); | |
if (isset($_GET['api'])) { | |
header('Content-Type: application/json; charset="UTF-8"'); | |
echo $favList; exit; | |
} | |
$favList = json_decode($favList, true); | |
if ($order == 2) usort($favList['data'], function ($a, $b) {return $a['last_ep_publish_time']>$b['last_ep_publish_time']?-1:1;}); | |
$hasPrevPage = $page > 1; | |
$hasNextPage = count($favList['data']) >= $pagesize; | |
$hasNextPage = true; // 列表中某页下架漫画会导致该页不足pageSize个……哪个弱智写的先select再filter的?? | |
?> | |
<!DOCTYPE html><html> | |
<head> | |
<meta charset="UTF-8" name="viewport" content="width=device-width,user-scalable=no"> | |
<meta name="format-detection" content="telephone=no" /> | |
<meta name="referrer" content="never"> | |
<title>书架 - bilibili漫画 阅读器 - BiliPlus</title> | |
<script src="materialize.min.js" defer></script> | |
<script> | |
document.addEventListener('DOMContentLoaded', function () { | |
var elems = document.querySelectorAll('select'); | |
var instances = M.FormSelect.init(elems, {}); | |
if (Date.now() - parseInt(localStorage.oauthTime) > 24 * 60 * 60 * 1000 || localStorage.oauthTime == undefined) { | |
var s = document.createElement('script'); | |
s.src = '/login?act=expiretime&days=60'; | |
document.body.appendChild(s); | |
s.remove(); | |
} | |
}); | |
</script> | |
<link rel="stylesheet" href="materialize.min.css" /> | |
<style> | |
body {width:95%;max-width:600px;margin:5px auto;background:#EFEFF4;cursor:default} | |
.comic-cover{height:200px} | |
.disabled{pointer-events:none} | |
@media (prefers-color-scheme: dark) { | |
body {background:black;color:#DDD} | |
.card,.dropdown-content {background:#1c1c1c} | |
.select-wrapper input.select-dropdown {color:white} | |
.select-wrapper .caret {fill:rgba(255,255,255,0.87)} | |
.pagination li a {color:white} | |
img {filter:brightness(0.8)} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="col s12"> | |
<h2>书架</h2> | |
<p class="row"> | |
<span class="col s6">© Copyright <a href="https://manga.bilibili.com/m/" target="_blank">bilibili</a> | |
<br><br> | |
<a href="?act=show_agreement">变更缓存设置</a><br> | |
<a href="?act=list_purchased">查看已购漫画</a> | |
</span> | |
<span class="input-field col s4"> | |
<select autocomplete="off" onchange="location.href=location.href+(location.href.indexOf('?')==-1?'?':'&')+'order='+this.value"> | |
<option <?php if ($order == 1) echo 'selected ' ?>value="add">追漫</option> | |
<option <?php if ($order == 2) echo 'selected ' ?>value="last_update">更新</option> | |
<option <?php if ($order == 3) echo 'selected ' ?>value="last_read">阅读</option> | |
</select> | |
<label>排序:</label> | |
</span> | |
</p> | |
<div> | |
<?php require 'sw_script.php'; ?> | |
</div> | |
<?php | |
//$userInit = ['data'=>['recieved_coupons'=>[['id'=>580482,'amount'=>10,'expire_time'=>'2019-12-10 10:36:05','reason'=>'大会员特权','type'=>'全场券','ctime'=>'']]]]; | |
// recieved??? | |
if (!empty($getVipReward) && $getVipReward['code'] === 0) { | |
?> | |
<div class="card horizontal"> | |
<div class="card-stacked"> | |
<div class="card-content"> | |
<p>已领取本月 大会员漫读券 x <?php echo $getVipReward['data']['amount'];?></p> | |
<p><?php echo htmlspecialchars(json_encode($getVipReward, JSON_UNESCAPED_UNICODE+JSON_UNESCAPED_SLASHES));?></p> | |
</div> | |
</div> | |
</div> | |
<hr> | |
<?php | |
} | |
if ($accessRestricted) { | |
$smsValidationLink = 'https://passport.bilibili.com/api/login/sso?'.buildParam([ | |
'access_key' => $_COOKIE['access_key'], | |
'appkey' => $appkey, | |
'gourl' => 'https://manga.bilibili.com/blackboard/activity-XxM8KTtXNk.html', | |
'ts' => time(), | |
], $appsecret); | |
?> | |
<div class="card horizontal"> | |
<div class="card-stacked"> | |
<div class="card-content"> | |
<p>当前登录帐号已被限制获取漫画图片,请进入以下链接进行手机短信验证。</p> | |
<p>如已进行过验证操作请忽略本提示。</p> | |
<p>(此链接包含帐号的登录授权,请勿分享给他人)</p> | |
<p><a target="_blank" referrerpolicy="no-referrer" style="word-break:break-all" href="<?php echo $smsValidationLink;?>"><?php echo $smsValidationLink;?></a></p> | |
</div> | |
</div> | |
</div> | |
<hr> | |
<?php | |
} | |
if ($hasPrevPage || $hasNextPage) { | |
?> | |
<ul class="pagination row s12"> | |
<li class="col s2 waves-effect<?php if (!$hasPrevPage) echo ' disabled'; ?>"><a href="?page=<?php echo $page-1;?>"><</a></li> | |
<li class="col s8"></li> | |
<li class="col s2 waves-effect<?php if (!$hasNextPage) echo ' disabled'; ?>"><a href="?page=<?php echo $page+1;?>">></a></li> | |
</ul> | |
<?php | |
} | |
foreach ($favList['data'] as $item) { | |
?> | |
<div class="card horizontal"> | |
<div class="card-image"> | |
<img class="comic-cover" alt="封面" src="<?php echo wrapPicUrl(str_replace('http:','',$item['vcover']).'@400h.jpg');?>"> | |
</div> | |
<div class="card-stacked"> | |
<div class="card-content"> | |
<p><?php echo $item['title'];?></p> | |
<?php | |
$epPrice = getEpPrice($item['comic_id']); | |
if ($epPrice > 0) { | |
?> | |
<p>单章价格:<?php echo $epPrice;?> 漫币</p> | |
<?php | |
} | |
?> | |
<p>最近更新:[<?php echo $item['latest_ep_short_title'];?>] <?php echo $item['last_ep_publish_time'];?></p> | |
<p>上次阅读:<?php echo $item['last_ep_short_title'];?></p> | |
</div> | |
<div class="card-action"> | |
<a href="?act=detail&mangaid=<?php echo $item['comic_id'];?>">查看目录</a> | |
</div> | |
</div> | |
</div> | |
<?php | |
} | |
if (empty($favList['data'])) { | |
?> | |
<p style="text-align:center">书架里没有漫画了</p> | |
<?php | |
} | |
if (count ($favList['data']) > 3 && ($hasPrevPage || $hasNextPage)) { | |
?> | |
<ul class="pagination row s12"> | |
<li class="col s2 waves-effect<?php if (!$hasPrevPage) echo ' disabled'; ?>"><a href="?page=<?php echo $page-1;?>"><</a></li> | |
<li class="col s8"></li> | |
<li class="col s2 waves-effect<?php if (!$hasNextPage) echo ' disabled'; ?>"><a href="?page=<?php echo $page+1;?>">></a></li> | |
</ul> | |
<?php | |
} | |
?> | |
</div> | |
<script> | |
function preventTouchEndPropagation(e) { | |
e.stopPropagation() | |
} | |
window.addEventListener('load', function () { | |
setTimeout(function () { | |
document.querySelectorAll('li[id^="select-options"]').forEach(i => { | |
i.removeEventListener('touchend', preventTouchEndPropagation) | |
i.addEventListener('touchend', preventTouchEndPropagation) | |
}) | |
}, 100) | |
}) | |
</script> | |
</body> | |
</html> |
<?php | |
if (empty($_GET['mangaid']) || !preg_match('(^\d+$)', $_GET['mangaid']) || | |
empty($_GET['epid']) || !preg_match('(^\d+$)', $_GET['epid'])) { | |
errordie('无效id'); | |
} | |
if (isset($_GET['add_history'])) { | |
echo cget('http://manga.bilibili.com/twirp/bookshelf.v1.Bookshelf/AddHistory', [ | |
'post' => buildParam([ | |
'access_key'=>$_COOKIE['access_key'], | |
'actionKey'=>'appkey', | |
'appkey'=>$appkey, | |
'build'=>APPBUILD, | |
'comic_id'=>$_GET['mangaid'], | |
'device'=>'phone', | |
'ep_id'=>$_GET['epid'], | |
'mobi_app'=>'iphone_comic', | |
'platform'=>'ios', | |
'ts'=>time(), | |
'version'=>APPVER, | |
], $appsecret) | |
]); | |
exit; | |
} | |
function prettySize(int $s) { | |
if ($s <= 0) return null; | |
$units = ['B','KB','MB','GB']; | |
$unit = 0; | |
if ($s < 1001) return $s.'B'; | |
while ($s > 1000) { | |
$unit++; | |
$s/=1024; | |
} | |
return number_format($s, 2, '.', '') . $units[$unit]; | |
} | |
chdir(__DIR__); | |
$idxDb = new PDO('sqlite:index.db'); | |
$reseturl = false; | |
$mangaInfoStmt = $idxDb->prepare('SELECT * FROM manga_info WHERE id=?'); | |
$mangaInfoStmt->execute([$_GET['mangaid']]); | |
$mangaInfo = $mangaInfoStmt->fetch(PDO::FETCH_ASSOC); | |
if (empty($mangaInfo)) { | |
header('Location: ?act=detail&mangaid='.$_GET['mangaid'], true, 302); | |
exit; | |
} | |
$mangaTitle = $mangaInfo['title']; | |
$epSize = 0; | |
$epTitle = 'ep_id '.$_GET['epid']; | |
$epPages = 0; | |
$chapLink = ['']; | |
$epList = json_decode(brotli_uncompress($mangaInfo['list']), true); | |
usort($epList, function ($a, $b) {return $a['ord'] > $b['ord'] ? 1 : -1;}); | |
$epArr = array_map(function ($i) {return $i['id'];}, $epList); | |
$epIdx = array_search($_GET['epid'], $epArr); | |
if ($epIdx !== false) { | |
$epSize = $epList[$epIdx]['size']; | |
$epTitle = $epList[$epIdx]['short_title']; | |
$epPages = $epList[$epIdx]['image_count']; | |
$epFullTitle = trim($epList[$epIdx]['title']); | |
if ($epIdx > 0) { | |
$chapLink[] = '<a href="?act=read&mangaid='.$mangaInfo['id'].'&epid='.$epList[$epIdx - 1]['id'].'">上一话:'.$epList[$epIdx - 1]['short_title'].'</a>('.implode(' / ', array_filter([prettySize($epList[$epIdx - 1]['size']), $epList[$epIdx - 1]['image_count'] . '页'])).')'; | |
} | |
if ($epIdx < count($epArr) - 1) { | |
$chapLink[] = '<a href="?act=read&mangaid='.$mangaInfo['id'].'&epid='.$epList[$epIdx + 1]['id'].'">下一话:'.$epList[$epIdx + 1]['short_title'].'</a>('.implode(' / ', array_filter([prettySize($epList[$epIdx + 1]['size']), $epList[$epIdx + 1]['image_count'] . '页'])).')'; | |
} | |
} | |
$epTitle = implode(' - ', [$epTitle, $mangaTitle]); | |
$oversea = ''; | |
if (in_array($_GET['mangaid'], OVERSEA_MANGA_IDS)) { | |
$oversea = 'us'; | |
} else if (strpos($mangaTitle, '(境外版)') !== false) { | |
$oversea = 'us'; | |
} | |
$hitCache = false; | |
$hitStoredIdx = false; | |
$curl = curl_init(); | |
if (ALLOW_SHARING) { | |
$stmt = $idxDb->prepare('SELECT b.data AS `data`,a.hash,b.uid FROM index_data AS a,index_data_history AS b WHERE a.epid=? AND a.epid=b.epid AND a.hash=b.hash'); | |
$stmt->execute([$_GET['epid']]); | |
$row = $stmt->fetch(); | |
if (!empty($row) && !isset($_GET['refetch_index'])) { | |
$indexData = json_decode(brotli_uncompress($row['data']), true); | |
$hash = $row['hash']; | |
$uid = $row['uid']; | |
$stmt = $idxDb->prepare('SELECT accesskey FROM account WHERE uid=?'); | |
$stmt->execute([$uid]); | |
$row = $stmt->fetch(); | |
if (!empty($row)) { | |
$cacheUserAccessKey = $row['accesskey']; | |
} | |
$hitCache = true; | |
} | |
$storedIdxStmt = $idxDb->prepare('SELECT images FROM episode_index WHERE id=?'); | |
$storedIdxStmt->execute([$_GET['epid']]); | |
$storedIdx = $storedIdxStmt->fetch(); | |
if (!empty($storedIdx)) { | |
$hitCache = true; | |
$hitStoredIdx = true; | |
$imageInfoStmt = $idxDb->prepare('SELECT width,height FROM stored_image WHERE path=?'); | |
$indexData = []; | |
foreach (explode("\n", $storedIdx['images']) as $path) { | |
$path = pathinfo($path, PATHINFO_FILENAME) .'.webp'; | |
$imageInfoStmt->execute([$path]); | |
$row = $imageInfoStmt->fetch(); | |
$indexData[] = [ | |
'path' => $path, | |
'x' => $row['width'], | |
'y' => $row['height'], | |
]; | |
} | |
} | |
} | |
if (!$hitCache) { | |
if (isset($_GET['buy']) && isset($_GET['payid'])) { | |
$reseturl = true; | |
$payParam = [ | |
'access_key'=>$_COOKIE['access_key'], | |
'actionKey'=>'appkey', | |
'appkey'=>$appkey, | |
'build'=>APPBUILD, | |
'buy_method'=>$_GET['buy'], | |
'device'=>'phone', | |
'comic_id'=>$_GET['mangaid'], | |
'ep_id'=>$_GET['epid'], | |
'mobi_app'=>'iphone_comic', | |
'platform'=>PLATFORM, | |
'ts'=>time(), | |
'version'=>APPVER, | |
]; | |
$buy_endpoint = 'BuyEpisode'; | |
if ($_GET['buy'] == '2') { | |
// 漫读券购买 | |
$payParam['auto_pay_coupons_status'] = '2'; | |
$payParam['coupon_ids'] = $_GET['payid']; | |
$payParam['version'] = '6.1.0'; // ? | |
} else if ($_GET['buy'] == '3') { | |
// 漫币 | |
unset($payParam['comic_id']); | |
$payParam['auto_pay_gold_status'] = '2'; | |
$payParam['pay_amount'] = $_GET['payid']; | |
} else if ($_GET['buy'] == 'rent') { | |
// 限免卡 租赁 | |
unset($payParam['buy_method']); | |
$payParam['item_id'] = $_GET['payid']; | |
$buy_endpoint = 'RentEpisode'; | |
} else if ($_GET['buy'] == '4') { | |
// 等免 租赁 / 终端为购买 | |
} else if ($_GET['buy'] == '5') { | |
// 通用券 | |
unset($payParam['comic_id']); | |
$payParam['auto_pay_gold_status'] = '2'; | |
$payParam['pay_amount'] = $_GET['payid']; | |
} else { | |
errordie('购买参数无效'); | |
} | |
$payResult = json_decode(cget('http://manga.bilibili.com/twirp/comic.v1.Comic/'.$buy_endpoint, [ | |
'post' => buildParam($payParam, $appsecret), | |
'curl'=>$curl | |
]), true); | |
if ($payResult['code'] !== 0) { | |
errordie('购买出错: ['.$payResult['code'].']'.$payResult['msg']); | |
} | |
} | |
if (isset($_GET['refetch_index'])) $reseturl = true; | |
$idxUrl = json_decode(cget('http://manga.bilibili.com/twirp/comic.v1.Comic/GetImageIndex', [ | |
'post' => buildParam([ | |
'access_key'=>$_COOKIE['access_key'], | |
'actionKey'=>'appkey', | |
'appkey'=>$appkey, | |
'build'=>APPBUILD, | |
'device'=>'phone', | |
'ep_id'=>$_GET['epid'], | |
'mobi_app'=>'iphone_comic', | |
'platform'=>'ios', | |
'ts'=>time(), | |
'version'=>APPVER, | |
], $appsecret), | |
'oversea' => $oversea, | |
'curl'=>$curl | |
]), true); | |
if ($idxUrl['code'] == 1 && $idxUrl['msg'] == 'need buy episode') { | |
$buyInfo = json_decode(cget('http://manga.bilibili.com/twirp/comic.v1.Comic/GetEpisodeBuyInfo', [ | |
'post' => buildParam([ | |
'access_key'=>$_COOKIE['access_key'], | |
'actionKey'=>'appkey', | |
'appkey'=>$appkey, | |
'build'=>APPBUILD, | |
'device'=>'phone', | |
'ep_id'=>$_GET['epid'], | |
'mobi_app'=>'iphone_comic', | |
'platform'=>PLATFORM, | |
'ts'=>time(), | |
'version'=>APPVER, | |
], $appsecret), | |
'curl'=>$curl | |
]), true); | |
if ($buyInfo['code'] !== 0) { | |
errordie('获取购买信息出错: ['.$idxUrl['code'].']'.$idxUrl['msg']); | |
} | |
$buyInfo = $buyInfo['data']; | |
$hasRentItem = $buyInfo['remain_item']>0; | |
$canFreeRent = $buyInfo['allow_wait_free']; | |
$canFreeRentNow = $canFreeRent && $buyInfo['wait_free_at'] < date('Y-m-d H:i:s'); | |
errordie(implode('',[ | |
/* 标题 */'<h5>未购买章节</h5><h5>'.str_replace(['<','>'],['<','>'], $epTitle).'</h5>', | |
/* 大小 */'<p>'.implode(' / ', array_filter([prettySize($epSize), $epPages . '页'])).'</p>', | |
/* 钱包 */'<p>钱包:'.$buyInfo['remain_gold'].' 漫币 | '.$buyInfo['remain_coupon'].' 漫读券 | '.$buyInfo['remain_silver'].' 通用券 | '.$buyInfo['remain_item'].' 限免卡</p>', | |
/* 平台 */'<p>当前钱包平台:'.PLATFORM.' <a href="?'.$_SERVER['QUERY_STRING'].'&set_buy_platform='.['ios'=>'android','android'=>'ios'][PLATFORM].'" class="waves-effect waves-light btn">切换至'.['ios'=>'android','android'=>'ios'][PLATFORM].'</a></p>', | |
/* 跳转 */'<p><a href="/html/reply.htm#type=29&id='.$_GET['epid'].'&title='.rawurlencode($epTitle).'" target="_blank">评论区</a>'.implode(' | ', $chapLink).'</p>', | |
/* */'<p class="row s12"><a class="col s1"></a>', | |
/* 漫币 */'<a onclick="return confirmPay(this)" href="?'.$_SERVER['QUERY_STRING'].'&buy=3&payid='.$buyInfo['pay_gold'].'" class="col s4 waves-effect waves-light btn'.($buyInfo['remain_gold']<$buyInfo['pay_gold']?' disabled':'').'">'.$buyInfo['pay_gold'].' 漫币购买</a>', | |
/* */'<a class="col s2"></a>', | |
/* 漫读券 */'<a onclick="return confirmPay(this)" href="?'.$_SERVER['QUERY_STRING'].'&buy=2&payid='.implode(',', $buyInfo['recommend_coupon_ids']).'" class="col s4 waves-effect waves-light btn'.($buyInfo['remain_coupon']<1?' disabled':'').'">'.$buyInfo['ep_pay_coupons'].' 漫读券购买</a>', | |
/* */'<a class="col s1"></a></p>', | |
/* */'<p class="row s12"><a class="col s7"></a>', | |
/* 通用券 */'<a onclick="return confirmPay(this)" href="?'.$_SERVER['QUERY_STRING'].'&buy=5&payid='.$buyInfo['ep_silver'].'" class="col s4 waves-effect waves-light btn'.($buyInfo['remain_silver']<$buyInfo['ep_silver']?' disabled':'').'">'.$buyInfo['ep_silver'].' 通用券购买</a>', | |
/* */'<a class="col s1"></a></p>', | |
/* */'<p class="row s12"><a class="col s1"></a>', | |
/* 等免 */'<a href="?'.$_SERVER['QUERY_STRING'].'&buy=4&payid=0" class="col s4 waves-effect waves-light btn'.($canFreeRentNow?'':' disabled').'">'.($canFreeRentNow ? '免费租赁' : ($canFreeRent ? $buyInfo['wait_free_at'] . ' 免费' : '不可免费租赁')).'</a>', | |
/* */'<a class="col s2"></a>', | |
/* 限免卡 */'<a href="?'.$_SERVER['QUERY_STRING'].'&buy=rent&payid='.$buyInfo['recommend_item_id'].'" class="col s4 waves-effect waves-light btn'.($buyInfo['remain_item']<1?' disabled':'').'">1 限免卡租赁</a>', | |
/* */'<a class="col s1"></a></p>', | |
/* 预览 */'<p style="text-align:center"><img src="'.wrapPicUrl($buyInfo['first_image_url'].'?token='.$buyInfo['first_image_token']).'" width="500"></p>' | |
]), '<meta name="referrer" content="never" /><script src="materialize.min.js"></script><script>function confirmPay(e){return confirm("使用 "+e.textContent+"?")}</script><link rel="stylesheet" href="materialize.min.css" />'); | |
} | |
if ($idxUrl['code'] !== 0) { | |
errordie('获取索引出错: ['.$idxUrl['code'].']'.$idxUrl['msg']); | |
} | |
$indexData = $idxUrl['data']['images']; | |
$cacheUserAccessKey = $_COOKIE['access_key']; | |
if (ALLOW_SHARING) { | |
$indexDataStr = json_encode($indexData, JSON_UNESCAPED_SLASHES); | |
$picFileNames = array_map(function ($i) {return pathinfo($i['path'], PATHINFO_BASENAME);}, $indexData); | |
$store_stmt = $idxDb->prepare('REPLACE INTO index_data (epid,mangaid,hash) VALUES (?,?,?)'); | |
$data = brotli_compress($indexDataStr, 9); | |
$hash = crc32(json_encode($picFileNames)); | |
$idxDb->beginTransaction(); | |
$store_stmt->execute([$_GET['epid'], $_GET['mangaid'], $hash]); | |
$idxDb->prepare('REPLACE INTO index_data_history (epid,mangaid,hash,data,time,uid) VALUES (?,?,?,CAST(? AS BLOB),?,?)')->execute([$_GET['epid'], $_GET['mangaid'], $hash, $data, time(), $_COOKIE['mid']]); | |
$idxDb->prepare('REPLACE INTO index_path (epid,path) VALUES (?,?)')->execute([$_GET['epid'], explode('?', $idxUrl['data']['path'])[0]]); | |
$idxDb->prepare('UPDATE account SET accesskey=?, refresh=?, ip=?, keyexpire=? WHERE uid=? AND keyexpire<?')->execute([$_COOKIE['access_key'], $_COOKIE['refresh_token'], $_COOKIE['user_ip_enc'], $_COOKIE['expires'], $_COOKIE['mid'], $_COOKIE['expires']]); | |
// 将此前失效的章节重新移回 | |
$idxDb->prepare('INSERT OR IGNORE INTO index_data_history SELECT epid,mangaid,hash,data,time,uid FROM index_data_expired WHERE uid=?')->execute([$_COOKIE['mid']]); | |
$idxDb->prepare('DELETE FROM index_data_expired WHERE uid=?')->execute([$_COOKIE['mid']]); | |
$idxDb->commit(); | |
} | |
} | |
$urls = []; | |
$sizeResized = []; | |
$setWidth = 700; | |
$picFormat = 'jpg'; | |
$fullSizePic = false; | |
if (isset($_COOKIE['manga_pic_format'])) { | |
setcookie('manga_pic_format_http', $_COOKIE['manga_pic_format'], time() + 315360000, '/manga/', 'biliplus.com', true, true); | |
setcookie('manga_pic_format', 'delete', 1, '/manga/'); | |
$_COOKIE['manga_pic_format_http'] = $_COOKIE['manga_pic_format']; | |
} | |
if (isset($_COOKIE['manga_pic_format_http'])) { | |
switch ($_COOKIE['manga_pic_format_http']) { | |
case 'jpg-1400w': break; | |
case 'jpg-full': $fullSizePic = true; break; | |
case 'webp-1400w': $picFormat = 'webp'; break; | |
case 'webp-full': $picFormat = 'webp'; $fullSizePic = true; break; | |
} | |
} | |
/* | |
for ($i=0; $i<count($indexData['pics']); $i++) { | |
$append = '@'.($setWidth*2)."w.$picFormat"; | |
if ($fullSizePic) { | |
$append = "@.$picFormat"; | |
if ($picFormat == 'jpg') { | |
$append = ''; | |
} | |
} else if ($indexData['sizes'][$i]['cx'] <= $setWidth * 2) { | |
$append = "@.$picFormat"; | |
} | |
$urls[] = 'https://'.IMG_HOST.$indexData['pics'][$i].$append; | |
$sizeResized[] = [ | |
$setWidth, | |
round($setWidth / $indexData['sizes'][$i]['cx'] * $indexData['sizes'][$i]['cy']) | |
]; | |
} | |
*/ | |
/* | |
240327 无签名链接失效 | |
db更新: | |
ALTER TABLE "index_data" RENAME TO "index_data_old"; | |
ALTER TABLE "index_data_history" RENAME TO "index_data_history_old"; | |
CREATE TABLE "index_data" ( | |
"epid" integer NULL PRIMARY KEY AUTOINCREMENT, | |
"mangaid" integer NOT NULL DEFAULT '0', | |
"hash" integer NOT NULL DEFAULT '0' | |
); | |
CREATE TABLE "index_data_history" ( | |
"epid" integer NOT NULL, | |
"mangaid" integer NOT NULL, | |
"hash" integer NOT NULL, | |
"data" blob NOT NULL, | |
"urls" blob NOT NULL, | |
"time" integer NOT NULL DEFAULT '0', | |
PRIMARY KEY ("epid", "hash") | |
); | |
*/ | |
$imgUrlRequestParam = [ | |
'act' => 'get_image_url', | |
'epid' => $_GET['epid'], | |
'request_time' => time(), | |
]; | |
for ($i=0; $i<count($indexData); $i++) { | |
$append = '@'.($setWidth*2)."w.$picFormat"; | |
if ($fullSizePic) { | |
$append = "@.$picFormat"; | |
if ($picFormat == 'jpg') { | |
$append = ''; | |
} | |
} else if ($indexData[$i]['x'] <= $setWidth * 2) { | |
$append = "@.$picFormat"; | |
} | |
if (ALLOW_SHARING) { | |
$imgUrlRequestParam['file'] = pathinfo($indexData[$i]['path'], PATHINFO_BASENAME); | |
} else { | |
$imgUrlRequestParam['path'] = $indexData[$i]['path']; | |
} | |
$imgUrlRequestParam['append'] = $append; | |
//$urls[] = 'https://'.IMG_HOST.$indexData[$i]['path'].$append; | |
$urls[] = '?' . http_build_query($imgUrlRequestParam); | |
$sizeResized[] = [ | |
$setWidth, | |
round($setWidth / $indexData[$i]['x'] * $indexData[$i]['y']) | |
]; | |
} | |
$cleanGET = $_GET; | |
unset($cleanGET['buy']); | |
unset($cleanGET['payid']); | |
unset($cleanGET['refetch_index']); | |
$historyCnt = $idxDb->prepare('SELECT count(epid) FROM index_data_history WHERE epid=?'); | |
$historyCnt->execute([$_GET['epid']]); | |
$historyCnt = $historyCnt->fetch()[0]; | |
if ($historyCnt>1) $chapLink[] = '<a target="_blank" href="?act=diff&epid='.$_GET['epid'].'">索引历史比对</a>('.$historyCnt.')'; | |
$chapLink = implode(" | <!--\n |-->", $chapLink); | |
$idxTime = time(); | |
if (ALLOW_SHARING && !$hitStoredIdx) { | |
$idxTimeStmt = $idxDb->prepare('SELECT time FROM index_data_history WHERE epid=? AND hash=?'); | |
$idxTimeStmt->execute([$_GET['epid'], $hash]); | |
$idxTime = $idxTimeStmt->fetch()[0]; | |
} | |
?> | |
<!DOCTYPE html><html> | |
<head> | |
<meta charset="UTF-8" name="viewport" content="width=device-width"> | |
<meta name="format-detection" content="telephone=no" /> | |
<meta name="referrer" content="never"> | |
<title><?php echo str_replace(['<','>'],['<','>'], $epTitle);?> - 阅读 - bilibili漫画 阅读器 - BiliPlus</title> | |
<script src="materialize.min.js"></script> | |
<link rel="stylesheet" href="materialize.min.css" /> | |
<style> | |
body {width:100%;margin:5px 0;background:#EFEFF4;cursor:default;touch-action:manipulation;-webkit-text-size-adjust:none;} | |
.middle,#hoz-container:not(.hoz-container){width:95%;max-width:800px;margin:0 auto} | |
.comic-single{max-width:700px} | |
.hoz-container{direction: rtl;overflow-x:scroll;overflow-y:hidden;width:100%;min-width:750px;-webkit-overflow-scrolling:touch;transform-origin:0 0} | |
.hoz-container>div{text-align:center;width:<?php echo count($indexData) * 704;?>px;direction:rtl;margin-right:calc(50% - 350px)} | |
.hoz-container .comic-single{vertical-align:top} | |
@media (prefers-color-scheme: dark) { | |
body {background:black;color:#DDD} | |
img {filter:brightness(0.8)} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="col s12"> | |
<div class="middle"> | |
<h5><?php echo str_replace(['<','>'],['<','>'], $epTitle);?></h5> | |
<?php if (!empty($epFullTitle)) echo '<h6>'. $epFullTitle .'</h6>' ?> | |
<p>索引缓存于 <?php echo date('Y/m/d H:i:s', $idxTime)?></p> | |
<p>© Copyright <a href="https://manga.bilibili.com/m/mc<?php echo $_GET['mangaid']?>/<?php echo $_GET['epid']?>" target="_blank">bilibili</a></p> | |
<p><!-- | |
|--><a href="/html/reply.htm#type=29&id=<?php echo $_GET['epid']?>&title=<?php echo rawurlencode($epTitle)?>" target="_blank">评论区</a><span class="review"></span> | <!-- | |
|--><a href="?<?php echo http_build_query($cleanGET);?>&refetch_index">刷新索引</a> | <!-- | |
|--><a href="javascript:toggleScrolling();">切换滚动</a><?php echo $chapLink;?><!-- | |
|--></p> | |
<p> | |
<label style="color:inherit"><input type="checkbox" id="hozScrollFix" style="opacity:initial;position:initial;pointer-events:initial">横向滚动图片渲染修复</label> | |
<label style="color:inherit"><input type="checkbox" id="hozScrollHeight" style="opacity:initial;position:initial;pointer-events:initial">高度适配屏幕</label> | |
<label style="color:inherit"><input type="checkbox" id="reloadClickedPic" style="opacity:initial;position:initial;pointer-events:initial">重新加载点击的图片</label> | |
<label style="color:inherit"><select id="picFormatSelect" style="width:initial;display:initial;background-color:#CCCCCC;border-radius:initial;border-color:white;height:auto"> | |
<option selected disabled>图片质量</option> | |
<option value="jpg-1400w">jpg 1400宽度</option> | |
<option value="jpg-full">原图 全尺寸</option> | |
<option value="webp-1400w">webp 1400宽度</option> | |
<option value="webp-full">webp 全尺寸</option> | |
</select></label> | |
</p> | |
</div> | |
<div id="hoz-container"><div style="text-align:center;font-size:0"><!-- | |
<?php | |
for ($i=0; $i < count($indexData); $i++) { | |
?> | |
|--><img class="comic-single" title="<?php echo str_pad($i+1, 3, '0', STR_PAD_LEFT);?>" width="<?php echo $sizeResized[$i][0]?>" height="<?php echo $sizeResized[$i][1]?>" _src="<?php echo $urls[$i];?>"><!-- | |
<?php | |
} | |
?> | |
|--></div></div> | |
<div class="middle"><p><!-- | |
|--><a href="/html/reply.htm#type=29&id=<?php echo $_GET['epid']?>&title=<?php echo rawurlencode($epTitle)?>" target="_blank">评论区</a><span class="review"></span><?php echo $chapLink;?><!-- | |
|--></p></div> | |
</div> | |
<div class="fixed-action-btn"><div style="background:rgba(255,255,255,.6);text-align:center;font-family:Arial;color:#000;padding:2px 8px"><span id="page">1</span><br>/<br><?php echo count($indexData);?></div></div> | |
<script> | |
function review_count(data){if(data.code==0){[].slice.call(document.getElementsByClassName('review')).forEach(function(i){i.textContent='('+data.data.count+')';})}} | |
window.img_lazyload=function(){var t=[].slice.call(document.getElementsByTagName("img"));t.forEach(function(t){if(t.hasAttribute("_src")){var e=t.getBoundingClientRect();e.bottom<-100||e.top>innerHeight+1000||e.right<-1000||e.left>innerWidth+100||(t.setAttribute("src",t.getAttribute("_src")),t.removeAttribute("_src"))}})},window.addEventListener("scroll",img_lazyload),window.addEventListener("resize",img_lazyload); | |
var singles = [].slice.call(document.getElementsByClassName("comic-single")); | |
var blankImg = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg=="; | |
singles.forEach(function (i) {i.addEventListener('click', imgViewSlide);i.addEventListener('load', hozFixLoad); i.addEventListener('error', loadError); if (i.offsetWidth < 700) i.src = blankImg}); | |
function loadError() { | |
this.loadFailed = true; | |
} | |
function imgViewSlide(e) { | |
if (reloadClickedPic.checked) { | |
reloadClickedPic.checked = false; | |
var src = this.src; | |
this.src = 'about:blank'; | |
setTimeout(function() { | |
e.target.src = src; | |
}, 500); | |
return; | |
} | |
if (this.loadFailed) { | |
delete this.loadFailed; | |
this.src = this.src; | |
return; | |
} | |
var isBottomPartClick = e.clientY > innerHeight / 2, target = isBottomPartClick ? this.nextElementSibling : this.previousElementSibling; | |
if (target) { | |
if (hozScrolling) { | |
var containerBox = hozScrollEle.getBoundingClientRect(), targetBox = target.getBoundingClientRect(); | |
hozScrollEle.scrollLeft += targetBox.left / hozScrollScale + targetBox.width / 2 / hozScrollScale - containerBox.left / hozScrollScale - containerBox.width / 2 / hozScrollScale; | |
if (hozScrollHeight.checked) window.scrollTo(0, hozScrollEle.offsetTop); | |
} else { | |
window.scrollTo(0, target.offsetTop); | |
} | |
} | |
} | |
window.addEventListener('scroll', function () { | |
if (hozScrolling) return; | |
var line = innerHeight / 2 + scrollY, page = 1; | |
for (var i=0; i<singles.length; i++) { | |
if (singles[i].offsetTop > line) break; | |
page = singles[i].title | 0; | |
} | |
document.getElementById('page').textContent = page; | |
}); | |
var hozScrollEle = document.getElementById('hoz-container'); | |
var hozScrolling = false; | |
var hozFixTimeout = null; | |
function hozFixLoad() { | |
if (!hozScrolling || hozFixTimeout) return; | |
hozFix(); | |
} | |
function hozFix() { | |
if (!hozScrollFix.checked) return; | |
hozFixTimeout = 0; | |
hozScrolling = false; | |
hozScrollEle.classList.toggle('hoz-container'); | |
hozScrollEle.offsetWidth; | |
hozScrollEle.classList.toggle('hoz-container'); | |
setTimeout(function (){ hozScrolling = true; }, 100); | |
} | |
hozScrollEle.addEventListener('scroll', function () { | |
if (!hozScrolling) return; | |
clearTimeout(hozFixTimeout); | |
hozFixTimeout = setTimeout(hozFix, 150); | |
img_lazyload(); | |
var line = innerWidth / 2, page = 1; | |
for (var i=0; i<singles.length; i++) { | |
var box = singles[i].getBoundingClientRect(); | |
if (box.left + box.width < line) break; | |
page = singles[i].title | 0; | |
} | |
document.getElementById('page').textContent = page; | |
hozScrollPos = hozScrollEle.scrollLeft; | |
}) | |
var hozScrollPos = 0, hozScrollScale = 1; | |
window.addEventListener('resize', function () { | |
if (hozScrolling) { | |
if (hozScrollHeight.checked) { | |
var scale = Math.min(innerHeight / hozScrollEle.offsetHeight, 1); | |
hozScrollScale = scale; | |
hozScrollEle.style.transform = 'scale('+scale+')'; | |
hozScrollEle.style.width = (100 / scale) + '%'; | |
} else { | |
hozScrollScale = 1; | |
hozScrollEle.style.transform = ''; | |
hozScrollEle.style.width = ''; | |
} | |
hozScrollEle.scrollLeft = hozScrollPos; | |
} | |
}) | |
function toggleScrolling() { | |
hozScrollEle.classList.toggle('hoz-container'); | |
hozScrolling = hozScrollEle.classList.contains('hoz-container'); | |
localStorage.hozScroll = hozScrolling ? 1 : 0; | |
hozScrollPos = hozScrollEle.scrollLeft; | |
} | |
if (localStorage.hozScroll == 1) toggleScrolling(); | |
hozScrollFix.checked = localStorage.hozScrollFix == 1; | |
hozScrollFix.addEventListener('change', function () { | |
localStorage.hozScrollFix = this.checked ? 1 : 0; | |
}); | |
hozScrollHeight.checked = localStorage.hozScrollHeight == 1; | |
window.addEventListener('load', function () { | |
window.dispatchEvent(new Event('resize')); | |
}); | |
hozScrollHeight.addEventListener('change', function () { | |
window.dispatchEvent(new Event('resize')); | |
localStorage.hozScrollHeight = this.checked ? 1 : 0; | |
}) | |
picFormatSelect.addEventListener('change', function () { | |
document.cookie = "manga_pic_format=" + this.value + "; path=/manga/; max-age=315360000" | |
}) | |
if (Date.now() - parseInt(localStorage.oauthTime) > 24 * 60 * 60 * 1000 || localStorage.oauthTime == undefined) { | |
var s = document.createElement('script'); | |
s.src = '/login?act=expiretime&days=60'; | |
document.body.appendChild(s); | |
s.remove(); | |
} | |
<?php | |
if ($reseturl) { | |
?> | |
history.replaceState('', '', '?<?php echo http_build_query($cleanGET);?>') | |
<?php | |
} | |
?> | |
</script> | |
<script src="https://www.biliplus.com/api/reply?isCount=1&oid=<?php echo $_GET['epid'];?>&type=29&jsonp=jsonp&callback=review_count&_=<?php echo time();?>" async></script> | |
<script>fetch(location.href+'&add_history')</script> | |
</body> | |
</html> | |
<?php | |
<?php | |
if (empty($_GET['albumid']) || !preg_match('(^\d+$)', $_GET['albumid'])) { | |
errordie('无效albumid'); | |
} | |
$curl = curl_init(); | |
chdir(__DIR__); | |
$idxDb = new PDO('sqlite:index.db'); | |
$reseturl = false; | |
$infoStmt = $idxDb->prepare('SELECT a.*,b.data FROM album_info AS a,album_pic_data AS b WHERE a.id=? AND a.id=b.album_id AND a.pic_hash=b.hash'); | |
$infoStmt->execute([$_GET['albumid']]); | |
$info = $infoStmt->fetch(PDO::FETCH_ASSOC); | |
if (empty($info)) { | |
errordie('还未取得此特典信息'); | |
} | |
$epTitle = $info['title']; | |
$epFullTitle = $info['detail']; | |
$mangaid = $info['manga_id']; | |
$picPaths = json_decode(brotli_uncompress($info['data']), true); | |
$picInfoStmt = $idxDb->prepare('SELECT * FROM pic_size_info WHERE path IN ('.implode(',',array_map(function () {return '?';},$picPaths)).')'); | |
$picInfoStmt->execute($picPaths); | |
$picInfo = []; | |
while ($row = $picInfoStmt->fetch(PDO::FETCH_ASSOC)) { | |
$picInfo[$row['path']] = [$row['w'], $row['h']]; | |
} | |
$picLackInfo = array_filter($picPaths, function ($p) use($picInfo) { return !isset($picInfo[$p]); }); | |
if (!empty($picLackInfo)) { | |
$tokens = json_decode(cget('http://manga.bilibili.com/twirp/comic.v1.Comic/ImageToken', [ | |
'post' => buildParam([ | |
'access_key'=>$_COOKIE['access_key'], | |
'actionKey'=>'appkey', | |
'appkey'=>$appkey, | |
'build'=>APPBUILD, | |
'device'=>'phone', | |
'mobi_app'=>'iphone_comic', | |
'platform'=>'ios', | |
'ts'=>time(), | |
'urls'=>json_encode(array_map(function ($p) {return 'http://'.IMG_HOST.$p.'@999999w.jpg';}, $picLackInfo)), | |
'version'=>APPVER, | |
], $appsecret), | |
'curl' => $curl | |
]), true); | |
if (!$tokens || $tokens['code'] != 0) { | |
errordie('获取凭据出错: ['.$tokens['code'].']'.$tokens['msg']); | |
} | |
$picSrcInfo = []; | |
header('Content-Encoding: identity'); | |
?> | |
<!DOCTYPE html><html> | |
<head> | |
<meta charset="UTF-8" name="viewport" content="width=device-width,user-scalable=no"> | |
<meta name="format-detection" content="telephone=no" /> | |
<meta name="referrer" content="never"> | |
<title>获取图片信息 - 书籍 - bilibili漫画 阅读器 - BiliPlus</title> | |
<script src="materialize.min.js"></script> | |
<link rel="stylesheet" href="materialize.min.css" /> | |
<style> | |
body {width:95%;max-width:700px;margin:5px auto;background:#EFEFF4;cursor:default} | |
@media (prefers-color-scheme: dark) { | |
body {background:black;color:#DDD} | |
img {filter:brightness(0.8)} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="col s12"> | |
<h3><?php echo $epTitle;?></h3> | |
<p>正在获取图片长宽信息(共<?php echo count($picLackInfo)?>张)</p> | |
<p> | |
<?php | |
ob_flush(); | |
flush(); | |
$queue = curl_multi_init(); | |
$map = []; | |
$imgCurl = curl_init(); | |
curl_setopt_array($imgCurl, [ | |
CURLOPT_HEADER=>true, | |
CURLOPT_RETURNTRANSFER=>true, | |
CURLOPT_HTTPHEADER=>['Range: bytes=0-0'] | |
]); | |
$failed = []; | |
$insertInfo = []; | |
$i=0; $total = count($picLackInfo); | |
foreach ($tokens['data'] as $item) { | |
$ch = curl_copy_handle($imgCurl); | |
curl_setopt($ch, CURLOPT_URL, str_replace('https:','http:',$item['url']).'?token='.$item['token']); | |
preg_match('(//[^/]+([^\?@]+))', $item['url'], $m); | |
$map[(string)$ch] = $m[1]; | |
curl_multi_add_handle($queue, $ch); | |
} | |
do { | |
while (($code = curl_multi_exec($queue, $active)) == CURLM_CALL_MULTI_PERFORM) ; | |
if ($code != CURLM_OK) { break; } else { usleep(1e3); } | |
while ($done = curl_multi_info_read($queue)) { | |
$i++; | |
$path = $map[(string)$done['handle']]; | |
$result = curl_multi_getcontent($done['handle']); | |
curl_multi_remove_handle($queue, $done['handle']); | |
curl_close($done['handle']); | |
$headersArr = explode("\r\n", substr($result, 0, strpos($result, "\r\n\r\n"))); | |
$headers = []; | |
foreach ($headersArr as $h) { | |
$idx = strpos($h, ': '); | |
if ($idx !== false) { | |
$headers[strtolower(substr($h, 0, $idx))] = substr(trim($h), $idx + 2); | |
} | |
} | |
echo "\n <div>".$i.'/'.$total.' '.$path.'...'; | |
if (empty($headers['o-width']) || empty($headers['o-height'])) { | |
echo "失败</div>"; | |
@ob_flush();@flush(); | |
$failed[] = $path; | |
continue; | |
} | |
$insertInfo[] = [$path, $headers['o-width'], $headers['o-height']]; | |
$picInfo[$path] = [$headers['o-width'], $headers['o-height']]; | |
echo $headers['o-width'].'x'.$headers['o-height'].'</div>'; | |
@ob_flush();@flush(); | |
} | |
if ($active > 0) { | |
curl_multi_select($queue, 0.5); | |
} | |
} while ($active > 0); | |
curl_multi_close($queue); | |
$picInfoInsertStmt = $idxDb->prepare('INSERT OR IGNORE INTO pic_size_info (path,w,h) VALUES (?,?,?)'); | |
$idxDb->beginTransaction(); | |
for ($i=0; $i<count($insertInfo); $i++) { | |
$picInfoInsertStmt->execute($insertInfo[$i]); | |
} | |
$idxDb->commit(); | |
if (!empty($failed)) { | |
echo '<p>获取图片长宽时出现错误,请刷新重试</p>'; | |
} else { | |
echo '<p>正在进入阅读页……</p><meta http-equiv="refresh" content="0" />'; | |
} | |
?> | |
</div> | |
</body> | |
</html> | |
<?php | |
exit; | |
} | |
$urls = []; | |
$sizeResized = []; | |
$setWidth = 700; | |
$picFormat = 'jpg'; | |
$fullSizePic = false; | |
if (isset($_COOKIE['manga_pic_format_http'])) { | |
switch ($_COOKIE['manga_pic_format_http']) { | |
case 'jpg-1400w': break; | |
case 'jpg-full': $fullSizePic = true; break; | |
case 'webp-1400w': $picFormat = 'webp'; break; | |
case 'webp-full': $picFormat = 'webp'; $fullSizePic = true; break; | |
} | |
} | |
for ($i=0; $i<count($picPaths); $i++) { | |
$append = '@'.($setWidth*2)."w.$picFormat"; | |
if ($fullSizePic) { | |
$append = "@.$picFormat"; | |
if ($picFormat == 'jpg') { | |
$append = ''; | |
} | |
} else if ($picInfo[$picPaths[$i]][0] <= $setWidth * 2) { | |
$append = "@.$picFormat"; | |
} | |
$urls[] = 'https://'.IMG_HOST.$picPaths[$i].$append; | |
$sizeResized[] = [ | |
$setWidth, | |
round($setWidth / $picInfo[$picPaths[$i]][0] * $picInfo[$picPaths[$i]][1]) | |
]; | |
} | |
$tokens = json_decode(cget('http://manga.bilibili.com/twirp/comic.v1.Comic/ImageToken', [ | |
'post' => buildParam([ | |
'access_key'=>$_COOKIE['access_key'], | |
'actionKey'=>'appkey', | |
'appkey'=>$appkey, | |
'build'=>APPBUILD, | |
'device'=>'phone', | |
'mobi_app'=>'iphone_comic', | |
'platform'=>'ios', | |
'ts'=>time(), | |
'urls'=>json_encode($urls), | |
'version'=>APPVER, | |
], $appsecret), | |
'curl' => $curl | |
]), true); | |
if (!$tokens || $tokens['code'] != 0) { | |
errordie('获取凭据出错: ['.$tokens['code'].']'.$tokens['msg']); | |
} | |
$cleanGET = $_GET; | |
unset($cleanGET['buy']); | |
unset($cleanGET['payid']); | |
unset($cleanGET['refetch_index']); | |
?> | |
<!DOCTYPE html><html> | |
<head> | |
<meta charset="UTF-8" name="viewport" content="width=device-width"> | |
<meta name="format-detection" content="telephone=no" /> | |
<meta name="referrer" content="never"> | |
<title><?php echo str_replace(['<','>'],['<','>'], $epTitle);?> - 阅读 - bilibili漫画 阅读器 - BiliPlus</title> | |
<script src="materialize.min.js"></script> | |
<link rel="stylesheet" href="materialize.min.css" /> | |
<style> | |
body {width:100%;margin:5px 0;background:#EFEFF4;cursor:default;touch-action:manipulation;-webkit-text-size-adjust:none;} | |
.middle,#hoz-container:not(.hoz-container){width:95%;max-width:800px;margin:0 auto} | |
.comic-single{max-width:700px} | |
.hoz-container{direction: rtl;overflow-x:scroll;overflow-y:hidden;width:100%;min-width:750px;-webkit-overflow-scrolling:touch;transform-origin:0 0} | |
.hoz-container>div{text-align:center;width:<?php echo count($tokens['data']) * 704;?>px;direction:rtl;margin-right:calc(50% - 350px)} | |
.hoz-container .comic-single{vertical-align:top} | |
@media (prefers-color-scheme: dark) { | |
body {background:black;color:#DDD} | |
img {filter:brightness(0.8)} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="col s12"> | |
<div class="middle"> | |
<h5><?php echo str_replace(['<','>'],['<','>'], $epTitle);?></h5> | |
<?php if (!empty($epFullTitle)) echo '<h6>'. $epFullTitle .'</h6>' ?> | |
<p>© Copyright <a href="https://manga.bilibili.com/m/detail/mc<?php echo $mangaid?>" target="_blank">bilibili</a></p> | |
<p><!-- | |
|--><a href="javascript:toggleScrolling();">切换滚动</a><!-- | |
|--></p> | |
<p> | |
<label style="color:inherit"><input type="checkbox" id="hozScrollFix" style="opacity:initial;position:initial;pointer-events:initial">横向滚动图片渲染修复</label> | |
<label style="color:inherit"><input type="checkbox" id="hozScrollHeight" style="opacity:initial;position:initial;pointer-events:initial">高度适配屏幕</label> | |
<label style="color:inherit"><input type="checkbox" id="reloadClickedPic" style="opacity:initial;position:initial;pointer-events:initial">重新加载点击的图片</label> | |
</p> | |
</div> | |
<div id="hoz-container"><div style="text-align:center;font-size:0"><!-- | |
<?php | |
for ($i=0; $i < count($tokens['data']); $i++) { | |
?> | |
|--><img class="comic-single" title="<?php echo str_pad($i+1, 3, '0', STR_PAD_LEFT);?>" width="<?php echo $sizeResized[$i][0]?>" height="<?php echo $sizeResized[$i][1]?>" _src="<?php echo wrapPicUrl($tokens['data'][$i]['url'].'?token='.$tokens['data'][$i]['token'].'&no_cache=1');?>"><!-- | |
<?php | |
} | |
?> | |
|--></div></div> | |
</div> | |
<div class="fixed-action-btn"><div style="background:rgba(255,255,255,.6);text-align:center;font-family:Arial;color:#000;padding:2px 8px"><span id="page">1</span><br>/<br><?php echo count($tokens['data']);?></div></div> | |
<script> | |
window.img_lazyload=function(){var t=[].slice.call(document.getElementsByTagName("img"));t.forEach(function(t){if(t.hasAttribute("_src")){var e=t.getBoundingClientRect();e.bottom<-100||e.top>innerHeight+1000||e.right<-1000||e.left>innerWidth+100||(t.setAttribute("src",t.getAttribute("_src")),t.removeAttribute("_src"))}})},window.addEventListener("scroll",img_lazyload),window.addEventListener("resize",img_lazyload); | |
var singles = [].slice.call(document.getElementsByClassName("comic-single")); | |
var blankImg = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg=="; | |
singles.forEach(function (i) {i.addEventListener('click', imgViewSlide);i.addEventListener('load', hozFixLoad); i.addEventListener('error', loadError); if (i.offsetWidth < 700) i.src = blankImg}); | |
function loadError() { | |
this.loadFailed = true; | |
} | |
function imgViewSlide(e) { | |
if (reloadClickedPic.checked) { | |
reloadClickedPic.checked = false; | |
var src = this.src; | |
this.src = 'about:blank'; | |
setTimeout(function() { | |
e.target.src = src; | |
}, 500); | |
return; | |
} | |
if (this.loadFailed) { | |
delete this.loadFailed; | |
this.src = this.src; | |
return; | |
} | |
var isBottomPartClick = e.clientY > innerHeight / 2, target = isBottomPartClick ? this.nextElementSibling : this.previousElementSibling; | |
if (target) { | |
if (hozScrolling) { | |
var containerBox = hozScrollEle.getBoundingClientRect(), targetBox = target.getBoundingClientRect(); | |
hozScrollEle.scrollLeft += targetBox.left / hozScrollScale + targetBox.width / 2 / hozScrollScale - containerBox.left / hozScrollScale - containerBox.width / 2 / hozScrollScale; | |
if (hozScrollHeight.checked) window.scrollTo(0, hozScrollEle.offsetTop); | |
} else { | |
window.scrollTo(0, target.offsetTop); | |
} | |
} | |
} | |
window.addEventListener('scroll', function () { | |
if (hozScrolling) return; | |
var line = innerHeight / 2 + scrollY, page = 1; | |
for (var i=0; i<singles.length; i++) { | |
if (singles[i].offsetTop > line) break; | |
page = singles[i].title | 0; | |
} | |
document.getElementById('page').textContent = page; | |
}); | |
var hozScrollEle = document.getElementById('hoz-container'); | |
var hozScrolling = false; | |
var hozFixTimeout = null; | |
function hozFixLoad() { | |
if (!hozScrolling || hozFixTimeout) return; | |
hozFix(); | |
} | |
function hozFix() { | |
if (!hozScrollFix.checked) return; | |
hozFixTimeout = 0; | |
hozScrolling = false; | |
hozScrollEle.classList.toggle('hoz-container'); | |
hozScrollEle.offsetWidth; | |
hozScrollEle.classList.toggle('hoz-container'); | |
setTimeout(function (){ hozScrolling = true; }, 100); | |
} | |
hozScrollEle.addEventListener('scroll', function () { | |
if (!hozScrolling) return; | |
clearTimeout(hozFixTimeout); | |
hozFixTimeout = setTimeout(hozFix, 150); | |
img_lazyload(); | |
var line = innerWidth / 2, page = 1; | |
for (var i=0; i<singles.length; i++) { | |
var box = singles[i].getBoundingClientRect(); | |
if (box.left + box.width < line) break; | |
page = singles[i].title | 0; | |
} | |
document.getElementById('page').textContent = page; | |
hozScrollPos = hozScrollEle.scrollLeft; | |
}) | |
var hozScrollPos = 0, hozScrollScale = 1; | |
window.addEventListener('resize', function () { | |
if (hozScrolling) { | |
if (hozScrollHeight.checked) { | |
var scale = Math.min(innerHeight / hozScrollEle.offsetHeight, 1); | |
hozScrollScale = scale; | |
hozScrollEle.style.transform = 'scale('+scale+')'; | |
hozScrollEle.style.width = (100 / scale) + '%'; | |
} else { | |
hozScrollScale = 1; | |
hozScrollEle.style.transform = ''; | |
hozScrollEle.style.width = ''; | |
} | |
hozScrollEle.scrollLeft = hozScrollPos; | |
} | |
}) | |
function toggleScrolling() { | |
hozScrollEle.classList.toggle('hoz-container'); | |
hozScrolling = hozScrollEle.classList.contains('hoz-container'); | |
localStorage.hozScroll = hozScrolling ? 1 : 0; | |
hozScrollPos = hozScrollEle.scrollLeft; | |
} | |
if (localStorage.hozScroll == 1) toggleScrolling(); | |
hozScrollFix.checked = localStorage.hozScrollFix == 1; | |
hozScrollFix.addEventListener('change', function () { | |
localStorage.hozScrollFix = this.checked ? 1 : 0; | |
}); | |
hozScrollHeight.checked = localStorage.hozScrollHeight == 1; | |
window.addEventListener('load', function () { | |
window.dispatchEvent(new Event('resize')); | |
}); | |
hozScrollHeight.addEventListener('change', function () { | |
window.dispatchEvent(new Event('resize')); | |
localStorage.hozScrollHeight = this.checked ? 1 : 0; | |
}) | |
if (Date.now() - parseInt(localStorage.oauthTime) > 24 * 60 * 60 * 1000 || localStorage.oauthTime == undefined) { | |
var s = document.createElement('script'); | |
s.src = '/login?act=expiretime&days=60'; | |
document.body.appendChild(s); | |
s.remove(); | |
} | |
<?php | |
if ($reseturl) { | |
?> | |
history.replaceState('', '', '?<?php echo http_build_query($cleanGET);?>') | |
<?php | |
} | |
?> | |
</script> | |
</body> | |
</html> |
<?php | |
/* | |
240329 db修改 | |
CREATE TABLE account ( | |
`uid` INTEGER PRIMARY KEY, | |
`state` INTEGER NOT NULL, | |
`accesskey` TEXT NOT NULL, | |
`refresh` TEXT NOT NULL DEFAULT '', | |
`ip` TEXT NOT NULL DEFAULT '', | |
`keyexpire` INTEGER NOT NULL | |
); | |
DELETE FROM index_data; | |
DROP TABLE index_data_history; | |
CREATE TABLE "index_data_history" ( | |
"epid" integer NOT NULL, | |
"mangaid" integer NOT NULL, | |
"hash" integer NOT NULL, | |
"data" blob NOT NULL, | |
"time" integer NOT NULL DEFAULT '0', | |
"uid" integer NOT NULL DEFAULT '0', | |
PRIMARY KEY ("epid", "hash") | |
); | |
CREATE INDEX `index_data_history_uid` ON `index_data_history` (`uid`); | |
*/ | |
function showAgreementPage($isForced = false) { | |
$state = ''; | |
if (!$isForced) { | |
$state = '<p>当前'.(ALLOW_SHARING ? '已' : '未').'开启缓存</p>'; | |
} | |
$pageContent = [ | |
'<h5>是否要开启漫画缓存</h5>', | |
$state, | |
'<p class="row s12"><a class="col s1"></a>', | |
'<a onclick="return setSharingCookie(this)" data-sharing="on" href="?" class="col s4 waves-effect waves-light btn">开启</a>', | |
'<a class="col s2"></a>', | |
'<a onclick="return setSharingCookie(this)" data-sharing="off" href="?" class="col s4 waves-effect waves-light btn red">关闭</a>', | |
'<a class="col s1"></a></p>', | |
'<hr>', | |
'<p><ul class="browser-default">', | |
'<li>开启缓存即表示您同意并授权我们保存您的登录状态信息</li>', | |
'<li>章节预览功能需要开启缓存才可使用</li>', | |
'</ul></p>', | |
'<hr>', | |
'<p><ul class="browser-default">', | |
'<li>本站不会主动扫描您的所有购买记录进行缓存</li>', | |
'<li>如想要缓存某部漫画,进入此漫画的详细页点击「获取未缓存索引」,并等待「关闭」按钮出现即可</li>', | |
'<li>一般情况下无需使用「刷新全部索引」按钮</li>', | |
'</ul></p>', | |
]; | |
errordie(implode("\n", $pageContent), '<script src="materialize.min.js"></script><script>function setSharingCookie(e){document.cookie = "manga_sharing_js=" + e.dataset.sharing + "; path=/manga/; max-age=315360000"; location.reload(); return false}</script><link rel="stylesheet" href="materialize.min.css" />'); | |
} | |
function setSharingCookieIfNeeded($state) { | |
$val = $state ? 'on' : 'off'; | |
if (empty($_COOKIE['manga_sharing']) || $_COOKIE['manga_sharing'] !== $val) { | |
setcookie('manga_sharing', $val, 0x7fffffff, '/manga/', 'biliplus.com', true, true); | |
} | |
} | |
$db = new PDO('sqlite:index.db'); | |
// 包含js设置的cookie | |
if (!empty($_COOKIE['manga_sharing_js'])) { | |
$value = $_COOKIE['manga_sharing_js']; | |
$stmt = $db->prepare('INSERT INTO account (`uid`, `state`, `accesskey`, `keyexpire`) VALUES (?,?,"",0) ON CONFLICT(`uid`) DO UPDATE SET `state` = excluded.`state`'); | |
$stmt->execute([$_COOKIE['mid'], $value === 'on' ? 1 : 0]); | |
setcookie('manga_sharing_js', 'delete', 1, '/manga/'); | |
unset($_COOKIE['manga_sharing']); | |
} | |
// 没有cookie | |
$stmt = $db->prepare('SELECT state FROM account WHERE `uid` = ? LIMIT 1'); | |
$stmt->execute([$_COOKIE['mid']]); | |
$row = $stmt->fetch(PDO::FETCH_ASSOC); | |
$stmt->closeCursor(); | |
// 没有设置过,强制显示选择 | |
if (empty($row)) { | |
showAgreementPage(true); | |
exit; | |
} | |
$allowSharing = $row['state'] == 1; | |
setSharingCookieIfNeeded($allowSharing); | |
define('ALLOW_SHARING', $allowSharing); | |
/** @type{Cache} */ | |
let normalCache | |
/** @type{Cache} */ | |
let readerCache | |
/** @type{Cache} */ | |
let readerResCache | |
const NORMAL = 'normal' | |
const READER = 'reader' | |
const READER_RES = 'reader-res' | |
const VERSION = 5 | |
let enable = true | |
async function initCache() { | |
normalCache = await caches.open(NORMAL) | |
readerCache = await caches.open(READER) | |
readerResCache = await caches.open(READER_RES) | |
} | |
/** | |
* @param {Headers} h | |
* @returns {Headers} | |
*/ | |
function cloneHeaders(h) { | |
const c = new Headers() | |
for (const k of h.keys()) { | |
c.set(k, h.get(k)) | |
} | |
return c | |
} | |
const cacheQueue = {} | |
const requestStorage = {} | |
const readerResrouceLink = {} | |
/** | |
* | |
* @param {Request} req | |
* @param {Response} res | |
*/ | |
async function checkAndAddCache(req, res) { | |
const url = req.url | |
if (readerResrouceLink[url]) { | |
const queue = cacheQueue[url] | |
delete cacheQueue[url] | |
req = requestStorage[readerResrouceLink[url]] | |
await queue.cache.put(req, res.clone()) | |
queue.client.postMessage({cmd: 'cached', url: readerResrouceLink[url]}) | |
delete requestStorage[readerResrouceLink[url]] | |
delete readerResrouceLink[url] | |
} else if (requestStorage[url] !== undefined) { | |
const headerWithTime = cloneHeaders(req.headers) | |
headerWithTime.set('sw-cache-time', Date.now()) | |
req = new Request(req.url, { headers: headerWithTime }) | |
requestStorage[url] = req | |
} else if (cacheQueue[url]) { | |
const queue = cacheQueue[url] | |
delete cacheQueue[url] | |
const headerWithTime = cloneHeaders(req.headers) | |
headerWithTime.set('sw-cache-time', Date.now()) | |
req = new Request(req.url, { headers: headerWithTime }) | |
await queue.cache.put(req, res.clone()) | |
queue.client.postMessage({cmd: 'cached', url}) | |
} else if (shouldPutNormalCache(req)) { | |
const headerWithTime = cloneHeaders(req.headers) | |
headerWithTime.set('sw-cache-time', Date.now()) | |
req = new Request(req.url, { headers: headerWithTime }) | |
const normalCache = await caches.open(NORMAL) | |
await normalCache.put(req, res.clone()) | |
} | |
} | |
/** | |
* @param {Request} req | |
*/ | |
function shouldPutNormalCache(req) { | |
if (req.mode == 'navigate') return true | |
if (['script', 'style'].indexOf(req.destination) !== -1) return true | |
if (req.destination == 'image') { | |
if (/hdslb\.com/.test(req.url) && !/\/manga\//.test(req.url)) return true | |
} | |
return false | |
} | |
/** | |
* @param {Cache} cache | |
* @returns {Promise<string[]>} | |
*/ | |
function clearCache(cache) { | |
return cache.keys().then(keys => ( | |
Promise.all(keys.map(key => cache.delete(key).then(b => key.url))) | |
)) | |
} | |
function clearAllCache() { | |
return Promise.all([NORMAL, READER, READER_RES].map(n => ( | |
caches.open(n).then(clearCache).then(r => ([n, r])) | |
))) | |
} | |
/** | |
* @param {Cache} cache | |
*/ | |
function clearCacheBefore(cache, time) { | |
return cache.keys().then(keys => ( | |
Promise.all(keys.filter(key => (key.headers.get('sw-cache-time') < time)).map(key => cache.delete(key).then(b => key.url))) | |
)) | |
} | |
/** | |
* | |
* @param {FetchEvent} e | |
* @returns {Promise<Response>} | |
*/ | |
async function processFetchEvent(e) { | |
const forcedCacheEntry = await caches.match(e.request, {cacheName: READER}) | |
if (forcedCacheEntry) return forcedCacheEntry.clone() | |
const forcedResCacheEntry = await caches.match(e.request, {cacheName: READER_RES}) | |
if (forcedResCacheEntry) return forcedResCacheEntry.clone() | |
let netErr = null | |
const netResponse = await fetch(e.request).catch(e => netErr = e) | |
if (!netErr && (netResponse.ok || netResponse.type == 'opaque')) { | |
await checkAndAddCache(e.request, netResponse) | |
return netResponse | |
} else { | |
const backupCacheEntry = await caches.match(e.request, {cacheName: NORMAL}) | |
if (backupCacheEntry) { | |
return backupCacheEntry.clone() | |
} else { | |
if (netErr) throw netErr | |
return netResponse | |
} | |
} | |
} | |
self.addEventListener('fetch', async e => { | |
if (!enable) return | |
// service worker运行超过30秒会被浏览器杀掉。。。 | |
if (/act=batch_index/.test(e.request.url)) return | |
e.respondWith(processFetchEvent(e)) | |
}) | |
self.addEventListener('install', async e => { | |
self.skipWaiting() | |
}) | |
self.addEventListener('activate', async e => { | |
e.waitUntil(initCache()) | |
}) | |
self.addEventListener('message', async e => { | |
const client = e.source | |
const msg = e.data | |
switch (msg.cmd) { | |
// request worker version | |
case 'version': { | |
client.postMessage({cmd: 'version', data: VERSION}) | |
client.postMessage({cmd: 'enable-status'}) | |
return | |
} | |
// update enable status | |
// { cmd: 'enable-status', enable: bool } | |
case 'enable-status': { | |
enable = msg.enable | |
if (!enable) { | |
const clearedEntries = Object.fromEntries(await clearAllCache()) | |
client.postMessage({cmd: 'cleared-cache', entries: clearedEntries}) | |
} | |
return | |
} | |
// cache next fetched reader web page | |
// { cmd: 'cache-reader-page', url: page_url } | |
case 'cache-reader-page': { | |
let cache = await caches.open(READER) | |
cacheQueue[msg.url] = {client, cache} | |
return | |
} | |
// cache next fetched reader resources | |
// { cmd: 'cache-reader-resources', urls: [page_img_url] } | |
case 'cache-reader-resources': { | |
let cache = await caches.open(READER_RES) | |
msg.urls.forEach(url => { | |
cacheQueue[url] = {client, cache} | |
}) | |
return | |
} | |
case 'create-reader-resources-key': { | |
msg.urls.forEach(url => { | |
requestStorage[url] = null; | |
}) | |
return | |
} | |
case 'link-reader-resources': { | |
let cache = await caches.open(READER_RES) | |
cacheQueue[msg.respUrl] = {client, cache} | |
readerResrouceLink[msg.respUrl] = msg.reqUrl | |
return | |
} | |
// clear expired cache | |
// { cmd: 'clear-stale-cache', keys: {key: day} } | |
case 'clear-stale-cache': { | |
const clearedEntries = {} | |
await Promise.all(Object.keys(msg.data).map(async cacheKey => { | |
const cache = await caches.open(cacheKey) | |
clearedEntries[cacheKey] = await clearCacheBefore(cache, Date.now() - msg.data.keys[cacheKey] * 24 * 3600e3) | |
})) | |
client.postMessage({cmd: 'cleared-cache', entries: clearedEntries}) | |
return | |
} | |
// clear all cache | |
// { cmd: 'clear-stale-cache'} | |
case 'clear-all-cache': { | |
const clearedEntries = Object.fromEntries(await clearAllCache()) | |
client.postMessage({cmd: 'cleared-cache', entries: clearedEntries}) | |
return | |
} | |
// clear cache entries | |
// { cmd: 'clear-stale-cache', keys: {key: [url]} } | |
case 'clear-cache-entries': { | |
const clearedEntries = {} | |
await Promise.all(Object.keys(msg.keys).map(async cacheKey => { | |
const cache = await caches.open(cacheKey) | |
clearedEntries[cacheKey] = await Promise.all(msg.keys[cacheKey].map(i => ( | |
delete cacheQueue[i], cache.delete(i).then(b => i) | |
))) | |
})) | |
client.postMessage({cmd: 'cleared-cache', entries: clearedEntries}) | |
return | |
} | |
} | |
}) |
<style> | |
#sw_control { | |
position: fixed; | |
top: 10px; | |
right: 20px; | |
background: #EFEFF4; | |
width: 250px; | |
height: 170px; | |
padding: 10px; | |
border: dashed 1px; | |
border-radius: 5px; | |
z-index: 10; | |
} | |
#sw_control.hide { | |
display: none | |
} | |
@media (prefers-color-scheme: dark) { | |
#sw_control { | |
background: black; | |
} | |
} | |
#sw_control.no-estimate #cache_size_container{display:none} | |
.cache-entry-row { | |
display:flex; | |
} | |
.cache-entry-row>* { | |
flex:0.1; | |
} | |
.cache-entry.downloading input { | |
pointer-events: none; | |
visibility: hidden; | |
} | |
.cache-entry .cache-episode-name { | |
flex: 1; | |
overflow: hidden; | |
white-space: pre; | |
text-overflow: ellipsis; | |
} | |
.cache-entry:not(.downloading) .cache-progress-text { | |
display:none; | |
} | |
.cache-entry:not(.downloading) .cache-progress-bar { | |
display:none; | |
} | |
.cache-progress-bar { | |
display: block; | |
flex: 0; | |
width: 100%; | |
height: 2px; | |
} | |
.cache-entry:not(.with-progress) .cache-progress-bar { | |
background-image: repeating-linear-gradient(90deg,rgb(69,208,30),rgb(69,208,30) 50%,transparent 50%,transparent 100%); | |
background-size: calc(100% / 3); | |
animation:cache_progress_anim 2s linear infinite; | |
} | |
.cache-entry.with-progress .cache-progress-bar { | |
background: rgb(69,208,30); | |
width:0; | |
} | |
@keyframes cache_progress_anim { | |
0% { | |
background-position: 0; | |
} | |
100% { | |
background-position: 100%; | |
} | |
} | |
#sw_cache_entries { | |
height: calc(150px - 4.5em); | |
overflow: hidden auto; | |
} | |
#sw_control.no-estimate #sw_cache_entries { | |
height: calc(150px - 3em); | |
} | |
</style> | |
<span id="sw_control_toggle_container" style="display:none"> | |
<label style="color:inherit"><input id="sw_control_toggle" autocomplete="off" type="checkbox" style="opacity:initial;position:initial;pointer-events:initial">显示缓存控制</label> | |
</span> | |
<div id="sw_control" class="hide"> | |
<div>缓存:</div> | |
<div id="cache_size_container">占用空间:<span id="cache_size">-- / --</span></div> | |
<div> | |
<label style="color:inherit"><input id="sw_toggle" autocomplete="off" type="checkbox" style="opacity:initial;position:initial;pointer-events:initial">启用缓存</label> | |
<input id="deleteCacheBtn" autocomplete="off" disabled type="button" value="删除选中缓存"> | |
</div> | |
<div id="sw_cache_entries"> </div> | |
</div> | |
<script> | |
'use strict' | |
var swReg | |
if (navigator.serviceWorker) { | |
// https://stackoverflow.com/a/34699901 | |
function de(b){var a,e={},d=b.split(""),f,c=f=d[0],g=[c],o,h=o=256;for(b=1;b<d.length;b++)a=d[b].charCodeAt(0),a=h>a?d[b]:e[a]?e[a]:f+c,g.push(a),c=a.charAt(0),e[o]=f+c,o++,f=a;return g.join("")} | |
function _(e,t,i){var a=null;if("text"===e)return document.createTextNode(t);a=document.createElement(e);for(var n in t)if("style"===n)for(var o in t.style)a.style[o]=t.style[o];else if("className"===n)a.className=t[n];else if("event"===n)for(var o in t.event)a.addEventListener(o,t.event[o]);else a.setAttribute(n,t[n]);if(i)if("string"==typeof i)a.innerHTML=i;else if(Array.isArray(i))for(var l=0;l<i.length;l++)null!=i[l]&&a.appendChild(i[l]);return a} | |
const NORMAL = 'normal' | |
const READER = 'reader' | |
const READER_RES = 'reader-res' | |
const swVer = 5 | |
const showSwControl = () => { | |
sw_control.classList.toggle('hide') | |
if (window.sw_action) { | |
sw_action.classList.toggle('hide') | |
} | |
} | |
const updateCacheSize = () => { | |
if (navigator.storage && navigator.storage.estimate) { | |
navigator.storage.estimate().then(s => { | |
const prettySize = s => { | |
const unit = Math.floor(Math.log2(s) / 10) | |
const num = unit > 0 ? (s / Math.pow(1024, unit)).toFixed(2) : s | |
return num + ['B', 'KiB', 'MiB', 'GiB'][unit] | |
} | |
cache_size.textContent = prettySize(s.usage) + ' / ' + prettySize(s.quota) | |
}) | |
} else { | |
sw_control.classList.add('no-estimate') | |
} | |
} | |
navigator.serviceWorker.getRegistration().then(function saveSwReg(reg) { | |
if (!reg) { | |
navigator.serviceWorker.register('/manga/sw.js') | |
navigator.serviceWorker.ready.then(saveSwReg) | |
return | |
} | |
swReg = reg | |
sw_control_toggle_container.style.display = '' | |
window.addEventListener('focus', () => { | |
sw_toggle.checked = localStorage.swToggle !== 'off' | |
updateCacheSize() | |
}) | |
sw_toggle.addEventListener('change', () => { | |
if (!sw_toggle.checked) { | |
const entries = [...document.querySelectorAll('.cache-entry')] | |
if (entries.length) { | |
if (!confirm('清空并禁用缓存吗?')) { | |
sw_toggle.checked = true | |
return | |
} | |
} | |
entries.forEach(i => i.remove()) | |
for (let k in localStorage) { | |
if (k.substr(0, 18) == 'manga_cache_entry_') { | |
delete localStorage[k] | |
} | |
} | |
} | |
localStorage.swToggle = sw_toggle.checked ? 'on' : 'off' | |
reg.active.postMessage({cmd: 'enable-status', enable: localStorage.swToggle !== 'off'}) | |
}) | |
sw_control_toggle.addEventListener('change', showSwControl) | |
window.dispatchEvent(new Event('focus')) | |
updateCacheSize() | |
navigator.serviceWorker.addEventListener('message', e => { | |
const msg = e.data | |
//console.log(e) | |
switch (msg.cmd) { | |
case 'version': { | |
if (msg.data !== swVer) { | |
console.log('update sw') | |
reg.update() | |
} | |
return | |
} | |
case 'enable-status': { | |
reg.active.postMessage({cmd: 'enable-status', enable: localStorage.swToggle !== 'off'}) | |
return | |
} | |
// { cmd: 'cleared-cache', keys: {key: day} } | |
case 'cleared-cache': { | |
//console.log('cleared-cache', msg.entries) | |
sleep(500).then(updateCacheSize) | |
return | |
} | |
// { cmd: 'cached', url: item } | |
case 'cached': { | |
sleep(500).then(updateCacheSize) | |
return | |
} | |
default: { | |
console.log(msg.cmd, msg) | |
} | |
} | |
}) | |
reg.active.postMessage({cmd: 'version'}) | |
reg.active.postMessage({cmd: 'enable-status', enable: localStorage.swToggle !== 'off'}) | |
setTimeout(() => {reg.active.postMessage({cmd: 'clear-stale-cache', keys: { [NORMAL]: 7 }})}, 3000) | |
}) | |
const cacheDeleteSelection = [] | |
function pickCacheToDelete(e) { | |
const box = e.target | |
const target = box.parentNode.parentNode.parentNode | |
const idx = cacheDeleteSelection.indexOf(target) | |
if (idx == -1) { | |
cacheDeleteSelection.push(target) | |
box.checked = true | |
} else { | |
cacheDeleteSelection.splice(idx, 1) | |
box.checked = false | |
} | |
deleteCacheBtn[cacheDeleteSelection.length ? 'removeAttribute' : 'setAttribute']('disabled', '') | |
} | |
deleteCacheBtn.addEventListener('click', () => { | |
cacheDeleteSelection.forEach(i => { | |
const k = i.dataset.key | |
const [url, info] = decodeCachedInfo(k) | |
swReg.active.postMessage({ cmd: 'clear-cache-entries', keys: { [READER]: [url], [READER_RES]: info.pages }}) | |
delete localStorage[k] | |
i.remove() | |
}) | |
cacheDeleteSelection.splice(0, cacheDeleteSelection.length) | |
}) | |
window.getCachedEpisodeEntryElement = function(key, info) { | |
return _('div', { className: 'cache-entry', 'data-key': key }, [ | |
_('div', { className: 'cache-entry-row' }, [ | |
_('div', {}, [_('input', { type: 'checkbox', event: { change: pickCacheToDelete }, style: {opacity:'initial', position:'initial', pointerEvents:'initial'}})]), | |
_('div', { className: 'cache-episode-name', title: info.ep +'-'+ info.manga }, [_('text', info.ep +'-'+ info.manga)]), | |
_('div', { className: 'cache-progress-text' }), | |
]), | |
_('div', { className: 'cache-progress-bar' }) | |
]) | |
} | |
function decodeCachedInfo(k) { | |
try { | |
const info = JSON.parse(unescape(de(localStorage[k]))) | |
return [k.substr(18), info] | |
} catch (e) { | |
const info = JSON.parse(localStorage[k]) | |
return [decodeURIComponent(k.substr(18)), info] | |
} | |
} | |
Object.keys(localStorage).filter(k => k.substr(0, 18) == 'manga_cache_entry_').sort().forEach(k => { | |
const [url, info] = decodeCachedInfo(k) | |
const node = getCachedEpisodeEntryElement(k, info) | |
sw_cache_entries.appendChild(node) | |
}) | |
/** | |
* @param {number} msec | |
* @returns {Promise<void>} | |
*/ | |
window.sleep = function(msec) { | |
return new Promise((res, rej) => setTimeout(res, msec)) | |
} | |
window.validateCachedImage = function (url) { | |
return new Promise((res, rej) => { | |
const div = document.body.appendChild(_('div', {style:{position:'absolute',visibility:'hidden',pointerEvents:'none'}}, [ | |
_('img', {src:url, event:{load:e => { | |
if (e.target.offsetHeight > 0 && e.target.offsetWidth > 0) { | |
div.remove() | |
return res(true) | |
} | |
div.remove() | |
rej(false) | |
}, error: e=> { | |
div.remove() | |
rej(false) | |
}}}) | |
])) | |
}) | |
} | |
window.validateAllCache = async function () { | |
for (var k of Object.keys(localStorage).filter(k => k.substr(0, 18) == 'manga_cache_entry_').sort()) { | |
const [_, info] = decodeCachedInfo(k) | |
for (var url of info.pages) { | |
await validateCachedImage(url).catch(r=>{cache_message.textContent = `${info.epFull} ${url}\n`+cache_message.textContent}) | |
} | |
cache_message.textContent = `已验证 ${info.epFull}\n`+cache_message.textContent | |
} | |
} | |
} | |
</script> | |
<?php | |
if (!isset($_GET['act'])) { // fav page | |
/* | |
? > | |
<script> | |
if (navigator.serviceWorker) { | |
const NORMAL = 'normal' | |
const READER = 'reader' | |
const READER_RES = 'reader-res' | |
} | |
</script> | |
< ?php | |
*/ | |
} else if ($_GET['act'] == 'detail') { // comic detail page | |
?> | |
<style> | |
.cache_selected .contents-simple > a, .cache_selected .contents-full { | |
background-image: repeating-linear-gradient( | |
45deg, | |
rgba(80,80,80,0.4), | |
rgba(80,80,80,0.4) 5px, | |
rgba(150,150,150,0.4) 5px, | |
rgba(150,150,150,0.4) 10px | |
); | |
background-size: 999px 999px; | |
background-position: 50%; | |
animation: cache_bg_anim linear 10s infinite; | |
} | |
@keyframes cache_bg_anim { | |
0% { | |
background-position: 50%; | |
} | |
100% { | |
background-position: calc(50% + 141.42px); | |
} | |
} | |
#sw_action.hide { | |
display:none | |
} | |
#cache_message { | |
max-height: 5.5em; | |
white-space: pre-wrap; | |
overflow-y: auto; | |
-webkit-overflow-scrolling: touch; | |
} | |
</style> | |
<div id="sw_action" class="hide"> | |
<label style="color:inherit"><input id="choose_episode_toggle" autocomplete="off" type="checkbox" style="opacity:initial;position:initial;pointer-events:initial">选择缓存章节</label> | |
<input id="cache_episode" autocomplete="off" disabled type="button" style="opacity:initial;position:initial;pointer-events:initial" value="缓存"> | |
<input id="validate_caches" autocomplete="off" type="button" style="opacity:initial;position:initial;pointer-events:initial" value="验证缓存"> | |
<div id="cache_message"></div> | |
</div> | |
<script> | |
if (navigator.serviceWorker) setTimeout(() => { | |
// https://stackoverflow.com/a/34699901 | |
function en(c){var x='charCodeAt',b,e={},f=c.split(""),d=[],a=f[0],g=256;for(b=1;b<f.length;b++)c=f[b],null!=e[a+c]?a+=c:(d.push(1<a.length?e[a]:a[x](0)),e[a+c]=g,g++,a=c);d.push(1<a.length?e[a]:a[x](0));for(b=0;b<d.length;b++)d[b]=String.fromCharCode(d[b]);return d.join("")} | |
const NORMAL = 'normal' | |
const READER = 'reader' | |
const READER_RES = 'reader-res' | |
let choosingCacheEpisode = false | |
let working = false | |
let currentTaskInfo = null | |
contents.addEventListener('click', e => { | |
if (choosingCacheEpisode) { | |
e.stopPropagation() | |
e.preventDefault() | |
if (working) return | |
let target = e.target | |
while (contents.contains(target) && target.parentNode != contents) { | |
target = target.parentNode | |
} | |
if (!contents.contains(target)) return | |
target.classList.toggle('cache_selected') | |
if (document.getElementsByClassName('cache_selected').length) { | |
cache_episode.removeAttribute('disabled') | |
} else { | |
cache_episode.setAttribute('disabled', '') | |
} | |
} | |
}, { capture: true }) | |
choose_episode_toggle.addEventListener('change', () => { | |
if (working) return | |
choosingCacheEpisode = choose_episode_toggle.checked | |
if (!choosingCacheEpisode) { | |
[...document.getElementsByClassName('cache_selected')].forEach(n => n.classList.remove('cache_selected')) | |
cache_episode.setAttribute('disabled', '') | |
} | |
}) | |
const workKeep = e => { | |
if (working) { | |
e.preventDefault() | |
e.returnValue = '正在缓存章节' | |
} | |
} | |
const abortWork = e => { | |
if (working) { | |
working = false | |
if (!currentTaskInfo) return | |
swReg.active.postMessage({ cmd: 'clear-cache-entries', keys: { [READER]: [currentTaskInfo.url], [READER_RES]: currentTaskInfo.pages }}) | |
} | |
} | |
cache_episode.addEventListener('click', async () => { | |
if (working) return | |
if (localStorage.swToggle === 'off') return | |
working = true | |
addEventListener('beforeunload', workKeep, {capture: true}) | |
addEventListener('unload', abortWork) | |
const list = document.getElementsByClassName('cache_selected') | |
for (let ep of list) { | |
await cacheEpisode(ep) | |
} | |
removeEventListener('beforeunload', workKeep, {capture: true}) | |
removeEventListener('unload', abortWork) | |
working = false | |
choose_episode_toggle.checked = false | |
choose_episode_toggle.dispatchEvent(new Event('change')) | |
}) | |
/** | |
* @param {Element} ep | |
*/ | |
async function cacheEpisode(ep, attempt = 1) { | |
if (attempt > 3) return | |
const info = { | |
manga: document.querySelector('h3').textContent, | |
ep: ep.querySelector('.contents-simple .episode').textContent, | |
epFull: ep.querySelector('.contents-full .episode').textContent, | |
url: '', | |
pages: [] | |
} | |
const pageUrl = ep.querySelector('.contents-simple .episode').href | |
if (localStorage[`manga_cache_entry_${pageUrl}`] != undefined) { | |
cache_message.textContent = `${info.epFull} - 已缓存,跳过\n` + cache_message.textContent | |
return | |
} | |
info.url = pageUrl | |
currentTaskInfo = info | |
const taskNode = getCachedEpisodeEntryElement(`manga_cache_entry_${pageUrl}`, info) | |
const taskProgressText = taskNode.querySelector('.cache-progress-text') | |
const taskProgressBar = taskNode.querySelector('.cache-progress-bar') | |
taskNode.classList.add('downloading') | |
taskProgressText.textContent = '0/1' | |
sw_cache_entries.insertBefore(taskNode, sw_cache_entries.childNodes[0]) | |
cache_message.textContent = `${info.epFull} - 获取章节信息\n` + cache_message.textContent | |
swReg.active.postMessage({ cmd: 'cache-reader-page', url: pageUrl}) | |
const pageData = await fetch(pageUrl).then(r => r.text()).catch(e => '') | |
const pageDom = (new DOMParser).parseFromString(pageData, 'text/html') | |
if (pageDom.getElementById('hoz-container') == null) { | |
// failed | |
taskNode.remove() | |
cache_message.textContent = `${info.epFull} - 获取图片失败\n` + cache_message.textContent | |
swReg.active.postMessage({ cmd: 'clear-cache-entries', keys: { [READER]: [pageUrl] }}) | |
return | |
} | |
const pageImgs = [...pageDom.querySelectorAll('#hoz-container img')] | |
const anchor = document.createElement('a') | |
const pageImgUrls = [...new Set(pageImgs.map(i => i.getAttribute('_src')))].map(i => ((anchor.href = i), anchor.href)) | |
cache_message.textContent = `${info.epFull} - 共 ${pageImgUrls.length} 页,缓存中\n` + cache_message.textContent | |
info.pages = pageImgUrls | |
taskNode.classList.add('with-progress') | |
let finFetch = 0 | |
taskProgressBar.style.width = ((finFetch+1)/(pageImgUrls.length+1)*100)+'%' | |
taskProgressText.textContent = `1/${pageImgUrls.length+1}` | |
const cachedMsgListener = e => { | |
const msg = e.data | |
// { cmd: 'cached', url: item } | |
if (msg.cmd === 'cached') { | |
const item = msg.url | |
if (info.pages.indexOf(item) != -1) { | |
validateCachedImage(item).then(r => {finFetch++}).catch(r=>anyFailed = {message:'图片无法显示'}) | |
} | |
} | |
} | |
navigator.serviceWorker.addEventListener('message', cachedMsgListener) | |
swReg.active.postMessage({ cmd: 'create-reader-resources-key', urls: pageImgUrls}) | |
const tokenRequestHeader = new Headers | |
tokenRequestHeader.set('X-Fetch-Request', 'true') | |
let anyFailed = false | |
const abrtCtrl = new AbortController | |
const fetchTask = Promise.all(pageImgUrls.map(i => ( | |
fetch(i, { | |
signal: abrtCtrl.signal, | |
referrerPolicy: "no-referrer", | |
headers: tokenRequestHeader | |
}).then(r=>r.json()).then(r => { | |
if (!r.success) throw new Error(r.message) | |
swReg.active.postMessage({ cmd: 'link-reader-resources', reqUrl: i, respUrl: r.url}) | |
return fetch(r.url, { | |
signal: abrtCtrl.signal, | |
mode: "no-cors", | |
referrerPolicy: "no-referrer", | |
}) | |
}).catch(e => anyFailed = e) | |
))) | |
while (finFetch < pageImgUrls.length && !anyFailed) { | |
await sleep(50) | |
taskProgressBar.style.width = ((finFetch+1)/(pageImgUrls.length+1)*100)+'%' | |
taskProgressText.textContent = `${finFetch+1}/${pageImgUrls.length+1}` | |
} | |
if (anyFailed) { | |
// failed | |
abrtCtrl.abort() | |
navigator.serviceWorker.removeEventListener('message', cachedMsgListener) | |
taskNode.remove() | |
cache_message.textContent = `下载图片失败,第${attempt}尝试 - ${anyFailed.message}\n` + cache_message.textContent | |
swReg.active.postMessage({ cmd: 'clear-cache-entries', keys: { [READER]: [pageUrl], [READER_RES]: pageImgUrls }}) | |
await sleep(1000) | |
return cacheEpisode(ep, attempt+1) | |
} | |
localStorage[`manga_cache_entry_${pageUrl}`] = en(escape(JSON.stringify(info))) | |
currentTaskInfo = null | |
cache_message.textContent = `${info.epFull} - 缓存完成\n` + cache_message.textContent | |
taskNode.classList.remove('with-progress') | |
taskNode.classList.remove('downloading') | |
} | |
validate_caches.addEventListener('click', validateAllCache); | |
}) | |
</script> | |
<?php | |
} |