Skip to content

Instantly share code, notes, and snippets.

@esterTion
Last active April 17, 2024 16:13
Show Gist options
  • Star 24 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save esterTion/292e27b97884dfa58542308dd896ce38 to your computer and use it in GitHub Desktop.
Save esterTion/292e27b97884dfa58542308dd896ce38 to your computer and use it in GitHub Desktop.
BiliComicWebReader
shit title placeholder
<?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');
$idxPath = $idxDb->prepare('REPLACE INTO index_path (epid,path) VALUES (?,?)');
$accountKeyStore = $idxDb->prepare('UPDATE account SET accesskey=?, 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;
}
$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=?, 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['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}
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(' &amp; ', $discount_desc) ?> | <?php echo $detail['discount_end']?></p>
<?php
}?>
<?php if (!empty($detail['disable_coupon_amount'])) {
?>
<p>最新 <?php echo $detail['disable_coupon_amount'] ?> 章不可使用福利券</p>
<?php
}?>
<?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>&copy; 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=?');
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';
?>
|--><span><!--
|--><ruby class="contents-simple"><a class="episode block<?php if ($ep['is_locked'] && !$ep['is_in_free']) echo ' locked'; if ($idxCnt>1) echo ' multiple-version'?>" 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="episode<?php if ($ep['is_locked'] && !$ep['is_in_free']) echo ' locked'; if ($idxCnt>1) echo ' multiple-version'?>" 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--;
}
$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);
$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) {
$append = '@'.($setWidth*2).'w.jpg';
if ($indexData[$i]['x'] <= $setWidth * 2) {
$append = '@.jpg';
}
return [
'url' => 'https://'.IMG_HOST.$indexData[$i]['path'].$append,
'size' => [
$setWidth,
$setWidth / $indexData[$i]['x'] * $indexData[$i]['y']
]
];
}
$tokenMap = [];
$stmt = $idxDb->prepare('SELECT accesskey FROM account WHERE uid=?');
$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 (?,?,?)');
foreach ($imgUrl as $uid => $urls) {
$stmt->execute([$uid]);
$row = $stmt->fetch();
if (empty($row)) continue;
$tokens = json_decode(cget('http://manga.bilibili.com/twirp/comic.v1.Comic/ImageToken', [
'post' => buildParam([
'access_key'=>$row['accesskey'],
'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'] === 'invalid_argument' && 'bucket.u ' === substr($tokens['msg'], 0, 9)) {
// accesskey无效,删除该用户所有缓存
$moveStmt->execute([$uid]);
$moveStmt->closeCursor();
$deleteStmt->execute([$uid]);
$deleteCount = $idxDb->query('SELECT changes()')->fetch()[0];
$deleteStmt->closeCursor();
$expireLogStmt->execute([$uid, time(), $deleteCount]);
$expireLogStmt->closeCursor();
} else if (!$tokens || $tokens['code'] !== 0) {
errordie('获取凭据出错: ['.$tokens['code'].']'.$tokens['msg']);
}
foreach ($tokens['data'] as $item) {
$tokenMap[pathinfo(explode('?', $item['url'])[0], PATHINFO_BASENAME)] = $item;
}
}
?>
<!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>&copy; 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'])) {
$imgPath = pathinfo($ep['first_image']['path'], PATHINFO_BASENAME);
if (isset($tokenMap[$imgPath]))
$attr = 'src="'.wrapPicUrl($tokenMap[$imgPath]['url'].'?token='.$tokenMap[$imgPath]['token']).'" 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>
</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
require_once '../include/gzip.php';
require_once $root_prefix.'include/functions.php';
$appkey = 'da44a5d9227fa9ef';
$appsecret = 'ad875eed760f65ac5ade5f363ab05e42';
define('APPVER', '3.7.3');
define('APPBUILD', '873');
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;
}
}
}
require_once 'listfav.php';
<?php
$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();
$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">&copy; 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>
<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
$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();
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);
}
}
}
$favList = cget('http://manga.bilibili.com/twirp/bookshelf.v1.Bookshelf/ListFavorite', [
'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,
], $appsecret)
]);
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">&copy; 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 ($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>
<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;
$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;
}
}
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_id'] = $_GET['payid'];
} 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(['<','>'],['&lt','&gt;'], $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='.$buyInfo['recommend_coupon_id'].'" class="col s4 waves-effect waves-light btn'.($buyInfo['remain_coupon']<1?' disabled':'').'">1 漫读券购买</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=?, keyexpire=? WHERE uid=? AND keyexpire<?')->execute([$_COOKIE['access_key'], $_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")
);
*/
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";
}
$urls[] = 'https://'.IMG_HOST.$indexData[$i]['path'].$append;
$sizeResized[] = [
$setWidth,
round($setWidth / $indexData[$i]['x'] * $indexData[$i]['y'])
];
}
$usingAccessKey = $_COOKIE['access_key'];
if (ALLOW_SHARING) {
$usingAccessKey = $cacheUserAccessKey;
}
$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),
'curl' => $curl
]), true);
$cleanGET = $_GET;
unset($cleanGET['buy']);
unset($cleanGET['payid']);
unset($cleanGET['refetch_index']);
if (!$tokens || $tokens['code'] !== 0) {
if ($tokens && $tokens['code'] === 'invalid_argument' && 'bucket.u ' === substr($tokens['msg'], 0, 9)) {
// accesskey无效,删除该用户所有缓存
$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 (?,?,?)');
$moveStmt->execute([$uid]);
$moveStmt->closeCursor();
$deleteStmt->execute([$uid]);
$deleteCount = $idxDb->query('SELECT changes()')->fetch()[0];
$deleteStmt->closeCursor();
$expireLogStmt->execute([$uid, time(), $deleteCount]);
$expireLogStmt->closeCursor();
}
errordie('获取凭据出错: ['.$tokens['code'].']'.$tokens['msg'].'<br><a href="?'.http_build_query($cleanGET).'&refetch_index">刷新索引</a>');
}
$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) {
$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(['<','>'],['&lt','&gt;'], $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(['<','>'],['&lt','&gt;'], $epTitle);?></h5>
<?php if (!empty($epFullTitle)) echo '<h6>'. $epFullTitle .'</h6>' ?>
<p>索引缓存于 <?php echo date('Y/m/d H:i:s', $idxTime)?></p>
<p>&copy; 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($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 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($tokens['data']);?></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(['<','>'],['&lt','&gt;'], $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(['<','>'],['&lt','&gt;'], $epTitle);?></h5>
<?php if (!empty($epFullTitle)) echo '<h6>'. $epFullTitle .'</h6>' ?>
<p>&copy; 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,
`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 = 3
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 = {}
/**
*
* @param {Request} req
* @param {Response} res
*/
async function checkAndAddCache(req, res) {
const url = req.url
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 })
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)
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
}
// 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 = 3
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 pageImgUrls = [...new Set(pageImgs.map(i => i.getAttribute('_src')))]
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: 'cache-reader-resources', urls: pageImgUrls})
let anyFailed = false
const abrtCtrl = new AbortController
const fetchTask = Promise.all(pageImgUrls.map(i => (
fetch(i, {
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
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment