Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
BiliComicWebReader
shit title placeholder
<?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,data) VALUES (?,?,?,CAST(? AS BLOB))');
$history = $idxDb->prepare('INSERT OR IGNORE INTO index_data_history SELECT *,? as time FROM index_data WHERE epid=?');
$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}
</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>
<p>
<?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;
do {
try {
$idxDb->beginTransaction();
break;
} catch (Exception $e) {}
} while (1);
$store_stmt->execute([$realepid, $realmangaid, $hash, $data]);
$history->execute([time(), $ep['id']]);
$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();
}
?>
</p>
<p><a href="javascript:window.close()" class="col s4 waves-effect waves-light btn">关闭</a></p>
</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) {
$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}
</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>
<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['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['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>
<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>
</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>
<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 prettySize($ep['size'])?></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.addEventListener('change', function () {
for (var i = contents.children.length - 1; i>=0; i--) {
contents.appendChild(contents.children[i]);
}
})
if (Date.now() - parseInt(localStorage.oauthTime) > 24 * 60 * 60 * 1000 || localStorage.oauthTime == undefined) {
var s = document.createElement('script');
s.src = '/login?act=expiretime&days=8';
document.body.appendChild(s);
s.remove();
}
</script>
<script src="https://api.bilibili.com/x/reply/count?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;});
$epIdxStmt = $idxDb->prepare('SELECT data FROM index_data WHERE epid=?');
$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}
</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>
<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'];?>)</div><!--
<?php
}
?>
|--></div>
</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}
</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>
<script>
window.img_lazyload=function(){var t=[].slice.call(document.getElementsByTagName("img"));t.forEach(function(t){if(t.hasAttribute("_src")){var e=t.getBoundingClientRect();e.bottom<-100||e.top>innerHeight+1000||e.right<-1000||e.left>innerWidth+100||(t.setAttribute("src",t.getAttribute("_src")),t.removeAttribute("_src"))}})},window.addEventListener("scroll",img_lazyload),window.addEventListener("resize",img_lazyload),img_lazyload();
</script>
</body>
</html>
<?php
require_once '../include/gzip.php';
require_once $root_prefix.'include/functions.php';
$appkey = 'da44a5d9227fa9ef';
$appsecret = 'ad875eed760f65ac5ade5f363ab05e42';
define('APPVER', '2.6.1');
define('APPBUILD', '579');
define('IMG_HOST', 'manga.hdslb.com');
header('NO-CONVERT-IMG: 1');
header('Content-Type: text/html');
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}
</style>
</head>
<body>
<p><?php echo $reason;?></p>
</body>
</html><?php
exit;
}
function wrapPicUrl(string $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;
}
}
}
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();
$userInit = json_decode(cget('https://manga.bilibili.com/twirp/user.v1.User/GetInitInfo', [
'curl' => $curl,
'post' => buildParam([
'access_key'=>$_COOKIE['access_key'],
'actionKey'=>'appkey',
'appkey'=>$appkey,
'build'=>APPBUILD,
'device'=>'phone',
'mobi_app'=>'iphone_comic',
'order'=>$order,
'page_num'=>$page,
'page_size'=>$pagesize,
'platform'=>'ios',
'ts'=>time(),
'version'=>APPVER,
], $appsecret)
]), 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;
?>
<!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, {});
});
</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}
</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>
</div>
<?php
//$userInit = ['data'=>['recieved_coupons'=>[['id'=>580482,'amount'=>10,'expire_time'=>'2019-12-10 10:36:05','reason'=>'大会员特权','type'=>'全场券','ctime'=>'']]]];
// recieved???
if (!empty($userInit['data']['recieved_coupons'])) {
?>
<div class="card horizontal">
<div class="card-stacked">
<div class="card-content">
<p>收到 <?php echo array_reduce($userInit['data']['recieved_coupons'], function ($c, $i) {return $c + $i['amount'];}, 0);?> 张新漫读券</p>
<?php
foreach ($userInit['data']['recieved_coupons'] as $item) {
printf(' <p>[%s] %sx%d,%s 过期</p>', $item['reason'], $item['type'], $item['amount'], $item['expire_time']);
}
?>
</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');
}
function prettySize(int $s) {
$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'];
$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'];
$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>('.prettySize($epList[$epIdx - 1]['size']).'';
}
if ($epIdx < count($epArr) - 1) {
$chapLink[] = '<a href="?act=read&mangaid='.$mangaInfo['id'].'&epid='.$epList[$epIdx + 1]['id'].'">下一话:'.$epList[$epIdx + 1]['short_title'].'</a>('.prettySize($epList[$epIdx + 1]['size']).'';
}
}
$epTitle = implode(' - ', [$epTitle, $mangaTitle]);
$stmt = $idxDb->prepare('SELECT data,hash FROM index_data WHERE epid=?');
$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') {
// 漫币
$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 {
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),
'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>'.prettySize($epSize).'</p>',
/* 钱包 */'<p>钱包:'.$buyInfo['remain_gold'].' 漫币 | '.$buyInfo['remain_coupon'].' 漫读券 | '.$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 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 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 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><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,data) VALUES (?,?,?,CAST(? AS BLOB))');
$data = brotli_compress($indexDataStr, 9);
$hash = crc32($data);
$idxDb->beginTransaction();
$store_stmt->execute([$_GET['epid'], $_GET['mangaid'], $hash, $data]);
$idxDb->prepare('INSERT OR IGNORE INTO index_data_history SELECT *,? as time FROM index_data WHERE epid=?')->execute([time(), $_GET['epid']]);
$idxDb->prepare('REPLACE INTO index_path (epid,path) VALUES (?,?)')->execute([$_GET['epid'], explode('?', $idxUrl['data']['path'])[0]]);
$idxDb->commit();
}
$urls = [];
$sizeResized = [];
$setWidth = 700;
for ($i=0; $i<count($indexData['pics']); $i++) {
$append = '@'.($setWidth*2).'w.jpg';
if ($indexData['sizes'][$i]['cx'] <= $setWidth * 2) {
$append = '@.jpg';
}
$urls[] = 'https://'.IMG_HOST.$indexData['pics'][$i].$append;
$sizeResized[] = [
$setWidth,
$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']);
}
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),
'curl' => $curl
]);
$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}
</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>
</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"><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 = innerHeight / hozScrollEle.offsetHeight;
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=8';
document.body.appendChild(s);
s.remove();
}
<?php
if ($reseturl) {
?>
history.replaceState('', '', '?<?php echo http_build_query($cleanGET);?>')
<?php
}
?>
</script>
<script src="https://api.bilibili.com/x/reply/count?oid=<?php echo $_GET['epid'];?>&type=29&jsonp=jsonp&callback=review_count&_=<?php echo time();?>" async></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;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.