Skip to content

Instantly share code, notes, and snippets.

@esterTion
Last active August 25, 2023 09:33
  • Star 22 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
Star You must be signed in to star a gist
Embed
What would you like to do?
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']);
$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 (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('INSERT OR IGNORE INTO index_data_history (epid,mangaid,hash,data,time) VALUES (?,?,?,CAST(? AS BLOB),?)');
$hashChk = $idxDb->prepare('SELECT count(epid) FROM index_data WHERE epid=? AND hash=?');
$epChk = $idxDb->prepare('SELECT count(epid) FROM index_data WHERE epid=?');
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;
}
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;
}
$encryptedIndexData = cget(str_replace('https:', 'http:', wrapPicUrl($idxUrl['data']['host'] . $idxUrl['data']['path'])), ['curl'=>$idxCurl]);
preg_match('(/manga/(\d+)/(\d+)/data\.index)', $idxUrl['data']['path'], $idMatch);
$realmangaid = $idMatch[1];
$realepid = $idMatch[2];
$indexDataStr = decryptBiliComicIndex($encryptedIndexData, $realmangaid, $realepid);
if (!$indexDataStr) {
echo '下载索引出错</div>';
continue;
}
$indexData = @json_decode($indexDataStr, true);
if ($indexData == NULL) {
echo '下载索引出错</div>';
continue;
}
$data = brotli_compress($indexDataStr, 9);
$hash = crc32($data);
$hashChk->execute([$ep['id'], $hash]);
$updated = $hashChk->fetch()[0] != 1;
$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('INSERT OR IGNORE INTO index_data_history (epid,mangaid,hash,data,time) VALUES (?,?,?,CAST(? AS BLOB),?)');
$hashChk = $idxDb->prepare('SELECT count(epid) FROM index_data WHERE epid=? AND hash=?');
$epChk = $idxDb->prepare('SELECT count(epid) FROM index_data WHERE epid=?');
}
} while (1);
$store_stmt->execute([$realepid, $realmangaid, $hash]);
$history->execute([$realepid, $realmangaid, $hash, $data, time()]);
$idxDb->prepare('REPLACE INTO index_path (epid,path) VALUES (?,?)')->execute([$ep['id'], explode('?', $idxUrl['data']['path'])[0]]);
$idxDb->commit();
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
function decryptBiliComicIndex($data, $mangaid, $epid) {
if (substr($data, 0, 9) !== 'BILICOMIC') {
return false;
}
$body = substr($data, 9);
$size = strlen($body);
$out = str_repeat("\0", $size);
$key = [
$epid & 0xff,
$epid >> 8 & 0xff,
$epid >> 16 & 0xff,
$epid >> 24 & 0xff,
$mangaid & 0xff,
$mangaid >> 8 & 0xff,
$mangaid >> 16 & 0xff,
$mangaid >> 24 & 0xff
];
for ($i = 0; $i < $size; $i++) {
$byte = ord($body[$i]);
$byte ^= ($key[($i) % 8]);
$out[$i] = chr($byte);
}
return readBiliComicIndexZip(new MemoryStream($out));
}
function readBiliComicIndexZip(MemoryStream $out) {
$out->littleEndian = true;
$out->position = $out->size - 22;
$signature = $out->readData(4);
$num = $out->short;
$start = $out->short;
$numRecords = $out->short;
$total = $out->short;
$cdsize = $out->long;
$cdoffset = $out->long;
$commentlen = $out->short;
$out->position = $cdoffset + 4 + 2 * 6;
$crc = $out->ulong;
$compressedSize = $out->long;
$uncompressedSize = $out->long;
$out->position = 26;
$fnameLen = $out->short;
$extraLen = $out->short;
$out->position += $fnameLen + $extraLen;
$compressedData = $out->readData($compressedSize);
$data = gzinflate($compressedData);
if (crc32($data) !== $crc) {
return false;
}
return $data;
}
abstract class Stream {
abstract protected function read($length);
abstract public function seek($position);
abstract public function position();
abstract protected function getPos();
abstract protected function setPos($pos);
public function __get($name) {
switch($name) {
case 'position': return $this->getPos();
case 'bool': return $this->readBoolean();
case 'byte': return $this->readData(1);
case 'short': return $this->readInt16();
case 'ushort': return $this->readUint16();
case 'long': return $this->readInt32();
case 'ulong': return $this->readUint32();
case 'longlong': return $this->readInt64();
case 'ulonglong': return $this->readUint64();
case 'float': return $this->readFloat();
case 'double': return $this->readDouble();
case 'string': return $this->readStringToNull();
case 'line': return $this->readStringToReturn();
default: throw new Exception("Access undefined field ${name} of class ".get_class($this));
}
}
public function __set($name, $val) {
switch($name) {
case 'position': return $this->setPos($val);
default: throw new Exception("Assign value to undefined field ${name} of class ".get_class($this));
}
}
public $littleEndian = false;
public $size;
public function readStringToNull() {
$s = '';
while (ord($char = $this->read(1)) != 0) {
$s .= $char;
}
return $s;
}
public function readStringAt($pos) {
$current = $this->position;
$this->position = $pos;
$data = $this->string;
$this->position = $current;
return $data;
}
public function readStringToReturn() {
$s = '';
while ($this->position < $this->size && ($char = $this->read(1)) != "\n") {
$s .= $char;
}
return trim($s,"\r");
}
public function readBoolean() {
return ord($this->byte)>0;
}
public function readInt16() {
$uint = $this->readUint16();
$sint = unpack('s', pack('S', $uint))[1];
return $sint;
}
public function readUint16() {
$int = $this->read(2);
if (strlen($int) != 2) return 0;
return unpack($this->littleEndian?'v':'n', $int)[1];
}
public function readInt32() {
$uint = $this->readUint32();
$sint = unpack('l', pack('L', $uint))[1];
return $sint;
}
public function readUint32() {
$int = $this->read(4);
if (strlen($int) != 4) return 0;
return unpack($this->littleEndian?'V':'N', $int)[1];
}
public function readInt64() {
$uint = $this->readUint64();
$sint = unpack('q', pack('Q', $uint))[1];
return $sint;
}
public function readUint64() {
$int = $this->read(8);
if (strlen($int) != 8) return 0;
return unpack($this->littleEndian?'P':'J', $int)[1];
}
public function readFloat() {
$int = $this->read(4);
if (strlen($int) != 4) return 0;
if (!$this->littleEndian) $int = $int[3].$int[2].$int[1].$int[0];
return unpack(/*$this->littleEndian?'g':'G'*/ 'f', $int)[1];
}
public function readDouble() {
$int = $this->read(8);
if (strlen($int) != 8) return 0;
if (!$this->littleEndian) $int = $int[7].$int[6].$int[5].$int[4].$int[3].$int[2].$int[1].$int[0];
return unpack(/*$this->littleEndian?'e':'E'*/ 'd', $int)[1];
}
public function readData($size) {
if ($size <= 0) return '';
return $this->read($size);
}
public function readDataAt($pos, $size) {
$current = $this->position;
$this->position = $pos;
$data = $this->readData($size);
$this->position = $current;
return $data;
}
public function alignStream($alignment) {
$mod = $this->position % $alignment;
if ($mod != 0) {
$this->position += $alignment - $mod;
}
}
public function readAlignedString($len) {
$string = $this->readData($len);
$this->alignStream(4);
return $string;
}
}
class MemoryStream extends Stream {
private $data;
private $offset;
function __construct($data) {
$this->data = $data;
$this->size = strlen($data);
}
function __destruct() {
$this->data = NULL;
}
protected function read($length) {
$data = substr($this->data, $this->offset, $length);
$this->offset += $length;
return $data;
}
public function seek($position) {
$this->offset = $position;
}
public function write($newData) {
$this->data .= $newData;
$this->size += strlen($newData);
}
public function position() {
return $this->offset;
}
protected function getPos() {
return $this->offset;
}
protected function setPos($pos) {
$this->offset = $pos;
}
}
<?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">
<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=10';
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 (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 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;
$epIdx = json_decode(brotli_uncompress($epIdx['data']), true);
$idx = 0;
if (in_array($epIdx['pics'][$idx], ['/bfs/manga/dc7914d65771003337d24be6281c6189934b89c0.jpg', '/bfs/manga/e06419ed685fab1df07134fc4e0f2e9011808cb5.jpg'])) $idx++;
$img = getImgUrl($epIdx, $idx, 150);
$img['path'] = preg_replace('(.*//[^/]+(.+))', '$1', $img['url']);
$ep['first_image'] = $img;
$imgUrl[] = $img['url'];
}
unset($ep);
function getImgUrl(&$indexData, $i, $setWidth) {
$append = '@'.($setWidth*2).'w.jpg';
if ($indexData['sizes'][$i]['cx'] <= $setWidth * 2) {
$append = '@.jpg';
}
return [
'url' => 'https://'.IMG_HOST.$indexData['pics'][$i].$append,
'size' => [
$setWidth,
$setWidth / $indexData['sizes'][$i]['cx'] * $indexData['sizes'][$i]['cy']
]
];
}
$tokens = empty($imgUrl) ? ['code'=>0,'data'=>[]]: 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($imgUrl),
'version'=>APPVER,
], $appsecret)
]), true);
if (!$tokens || $tokens['code'] != 0) {
errordie('获取凭据出错: ['.$tokens['code'].']'.$tokens['msg']);
}
$tokenMap = [];
foreach ($tokens['data'] as $item) {
$tokenMap[preg_replace('(.*//[^/]+(.+))', '$1', $item['url'])] = $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) {
if (isset($ep['first_image'])) {
$imgPath = $ep['first_image']['path'];
$attr = 'src="'.wrapPicUrl($tokenMap[$imgPath]['url'].'?token='.$tokenMap[$imgPath]['token']).'" width="'.$ep['first_image']['size'][0].'" height="'.$ep['first_image']['size'][1].'"';
} else {
$attr = 'src="about:blank"';
}
?>
|--><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', []);
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}
@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) {
errordie('未登录<br><a href="javascript:localStorage.enablePlayback=\'on\',location.href=\'/login\'">登录</a>');
}
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': {
require_once 'diff.php';
exit;
}
case 'album': {
require_once 'album.php';
exit;
}
case 'read_album': {
require_once 'read_album.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;
$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=10';
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></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>
</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';
}
$stmt = $idxDb->prepare('SELECT b.data,a.hash FROM index_data AS a,index_data_history AS b WHERE a.epid=? AND a.epid=b.epid AND a.hash=b.hash');
$curl = curl_init();
$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'];
} else {
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']);
}
$idxCurl = curl_init();
$encryptedIndexData = cget(str_replace('https:', 'http:', wrapPicUrl($idxUrl['data']['host'] . $idxUrl['data']['path'])), ['curl'=>$idxCurl, 'curlOPT'=>[CURLOPT_FILETIME=>true]]);
$remoteTime = curl_getinfo($idxCurl, CURLINFO_FILETIME);
$idxCode = curl_getinfo($idxCurl, CURLINFO_HTTP_CODE);
if ($idxCode != 200) {
errordie('下载索引出错 HTTP '.$idxCode);
}
preg_match('(/manga/(\d+)/(\d+)/data\.index)', $idxUrl['data']['path'], $idMatch);
if ($_GET['mangaid'] != $idMatch[1]) {
$reseturl = true;
$_GET['mangaid'] = $idMatch[1];
}
$indexDataStr = decryptBiliComicIndex($encryptedIndexData, $idMatch[1], $idMatch[2]);
if (!$indexDataStr) {
errordie('下载索引出错');
}
$indexData = @json_decode($indexDataStr, true);
if ($indexData == NULL) {
errordie('下载索引出错');
}
$store_stmt = $idxDb->prepare('REPLACE INTO index_data (epid,mangaid,hash) VALUES (?,?,?)');
$data = brotli_compress($indexDataStr, 9);
$hash = crc32($data);
$idxDb->beginTransaction();
$store_stmt->execute([$_GET['epid'], $_GET['mangaid'], $hash]);
$idxDb->prepare('INSERT OR IGNORE INTO index_data_history (epid,mangaid,hash,data,time) VALUES (?,?,?,CAST(? AS BLOB),?)')->execute([$_GET['epid'], $_GET['mangaid'], $hash, $data, time()]);
$idxDb->prepare('REPLACE INTO index_path (epid,path) VALUES (?,?)')->execute([$_GET['epid'], explode('?', $idxUrl['data']['path'])[0]]);
$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'])
];
}
$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']);
}
$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);
$idxTimeStmt = $idxDb->prepare('SELECT time FROM index_data_history WHERE epid=? AND hash=?');
$idxTimeStmt->execute([$_GET['epid'], $hash]);
$idxTime = $idxTimeStmt->fetch()[0];
$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>索引缓存于 <?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=10';
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
function decryptBiliComicIndex($data, $mangaid, $epid) {
if (substr($data, 0, 9) !== 'BILICOMIC') {
return false;
}
$body = substr($data, 9);
$size = strlen($body);
$out = str_repeat("\0", $size);
$key = [
$epid & 0xff,
$epid >> 8 & 0xff,
$epid >> 16 & 0xff,
$epid >> 24 & 0xff,
$mangaid & 0xff,
$mangaid >> 8 & 0xff,
$mangaid >> 16 & 0xff,
$mangaid >> 24 & 0xff
];
for ($i = 0; $i < $size; $i++) {
$byte = ord($body[$i]);
$byte ^= ($key[($i) % 8]);
$out[$i] = chr($byte);
}
return readBiliComicIndexZip(new MemoryStream($out));
}
function readBiliComicIndexZip(MemoryStream $out) {
$out->littleEndian = true;
$out->position = $out->size - 22;
$signature = $out->readData(4);
$num = $out->short;
$start = $out->short;
$numRecords = $out->short;
$total = $out->short;
$cdsize = $out->long;
$cdoffset = $out->long;
$commentlen = $out->short;
$out->position = $cdoffset + 4 + 2 * 6;
$crc = $out->ulong;
$compressedSize = $out->long;
$uncompressedSize = $out->long;
$out->position = 26;
$fnameLen = $out->short;
$extraLen = $out->short;
$out->position += $fnameLen + $extraLen;
$compressedData = $out->readData($compressedSize);
$data = gzinflate($compressedData);
if (crc32($data) !== $crc) {
return false;
}
return $data;
}
abstract class Stream {
abstract protected function read($length);
abstract public function seek($position);
abstract public function position();
abstract protected function getPos();
abstract protected function setPos($pos);
public function __get($name) {
switch($name) {
case 'position': return $this->getPos();
case 'bool': return $this->readBoolean();
case 'byte': return $this->readData(1);
case 'short': return $this->readInt16();
case 'ushort': return $this->readUint16();
case 'long': return $this->readInt32();
case 'ulong': return $this->readUint32();
case 'longlong': return $this->readInt64();
case 'ulonglong': return $this->readUint64();
case 'float': return $this->readFloat();
case 'double': return $this->readDouble();
case 'string': return $this->readStringToNull();
case 'line': return $this->readStringToReturn();
default: throw new Exception("Access undefined field ${name} of class ".get_class($this));
}
}
public function __set($name, $val) {
switch($name) {
case 'position': return $this->setPos($val);
default: throw new Exception("Assign value to undefined field ${name} of class ".get_class($this));
}
}
public $littleEndian = false;
public $size;
public function readStringToNull() {
$s = '';
while (ord($char = $this->read(1)) != 0) {
$s .= $char;
}
return $s;
}
public function readStringAt($pos) {
$current = $this->position;
$this->position = $pos;
$data = $this->string;
$this->position = $current;
return $data;
}
public function readStringToReturn() {
$s = '';
while ($this->position < $this->size && ($char = $this->read(1)) != "\n") {
$s .= $char;
}
return trim($s,"\r");
}
public function readBoolean() {
return ord($this->byte)>0;
}
public function readInt16() {
$uint = $this->readUint16();
$sint = unpack('s', pack('S', $uint))[1];
return $sint;
}
public function readUint16() {
$int = $this->read(2);
if (strlen($int) != 2) return 0;
return unpack($this->littleEndian?'v':'n', $int)[1];
}
public function readInt32() {
$uint = $this->readUint32();
$sint = unpack('l', pack('L', $uint))[1];
return $sint;
}
public function readUint32() {
$int = $this->read(4);
if (strlen($int) != 4) return 0;
return unpack($this->littleEndian?'V':'N', $int)[1];
}
public function readInt64() {
$uint = $this->readUint64();
$sint = unpack('q', pack('Q', $uint))[1];
return $sint;
}
public function readUint64() {
$int = $this->read(8);
if (strlen($int) != 8) return 0;
return unpack($this->littleEndian?'P':'J', $int)[1];
}
public function readFloat() {
$int = $this->read(4);
if (strlen($int) != 4) return 0;
if (!$this->littleEndian) $int = $int[3].$int[2].$int[1].$int[0];
return unpack(/*$this->littleEndian?'g':'G'*/ 'f', $int)[1];
}
public function readDouble() {
$int = $this->read(8);
if (strlen($int) != 8) return 0;
if (!$this->littleEndian) $int = $int[7].$int[6].$int[5].$int[4].$int[3].$int[2].$int[1].$int[0];
return unpack(/*$this->littleEndian?'e':'E'*/ 'd', $int)[1];
}
public function readData($size) {
if ($size <= 0) return '';
return $this->read($size);
}
public function readDataAt($pos, $size) {
$current = $this->position;
$this->position = $pos;
$data = $this->readData($size);
$this->position = $current;
return $data;
}
public function alignStream($alignment) {
$mod = $this->position % $alignment;
if ($mod != 0) {
$this->position += $alignment - $mod;
}
}
public function readAlignedString($len) {
$string = $this->readData($len);
$this->alignStream(4);
return $string;
}
}
class MemoryStream extends Stream {
private $data;
private $offset;
function __construct($data) {
$this->data = $data;
$this->size = strlen($data);
}
function __destruct() {
$this->data = NULL;
}
protected function read($length) {
$data = substr($this->data, $this->offset, $length);
$this->offset += $length;
return $data;
}
public function seek($position) {
$this->offset = $position;
}
public function write($newData) {
$this->data .= $newData;
$this->size += strlen($newData);
}
public function position() {
return $this->offset;
}
protected function getPos() {
return $this->offset;
}
protected function setPos($pos) {
$this->offset = $pos;
}
}
<?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'])) {
switch ($_COOKIE['manga_pic_format']) {
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";
} else if ($picInfo[$picPaths[$i]][0] <= $setWidth * 2) {
$append = "@.$picFormat";
}
$urls[] = 'https://'.IMG_HOST.$picPaths[$i].$append;
$sizeResized[] = [
$setWidth,
$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=10';
document.body.appendChild(s);
s.remove();
}
<?php
if ($reseturl) {
?>
history.replaceState('', '', '?<?php echo http_build_query($cleanGET);?>')
<?php
}
?>
</script>
</body>
</html>
/** @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 = 2
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
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 = 2
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'})
reg.active.postMessage({cmd: 'clear-stale-cache', keys: { [NORMAL]: 7 }})
})
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