Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0f03ad9801 |
@@ -2,6 +2,9 @@
|
||||
|
||||
> 基于原作者v0.3.0代码修改
|
||||
|
||||
#### v0.3.0-2
|
||||
1. 新增页码URL参数`?page=1-3`、`?page=1,3,5`(需cookie;支持混用,如`?page=1-3,10,8`)
|
||||
|
||||
#### v0.3.0-1
|
||||
1. 新增视频/Live图链接以适配日记簿朋友圈
|
||||
2. 图片链接不再转URL编码及百度代理
|
||||
|
||||
+6
-1
@@ -52,6 +52,10 @@
|
||||
<button class="btn btn-default">生成</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="input-group" style="margin-top: 10px;">
|
||||
<span class="input-group-addon">页码</span>
|
||||
<input id="page-input" type="text" class="form-control" value="1" placeholder="1 / 7 / 1-10 / 1,3,5">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="loading" style="display: none;"><p class="text-center">加载中...</p></div>
|
||||
@@ -123,7 +127,8 @@ function showResult(uid) {
|
||||
}
|
||||
var baseUrl = location.href.split("?")[0];
|
||||
if (baseUrl.substr(-1, 1) !== "/") baseUrl += "/";
|
||||
var feedUrl = baseUrl + "rss/user/" + uid;
|
||||
var page = $.trim($("#page-input").val()) || "1";
|
||||
var feedUrl = baseUrl + "rss/user/" + uid + "?page=" + encodeURIComponent(page);
|
||||
|
||||
$("#foo").val(feedUrl);
|
||||
for (var reader in prefix) {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { describe, expect, test } from "@jest/globals";
|
||||
import { parsePageSpec, PageInvalidError } from "./page";
|
||||
|
||||
describe('page spec parser', () => {
|
||||
test('defaults to first page', () => {
|
||||
expect(parsePageSpec(undefined)).toEqual({
|
||||
pages: [1],
|
||||
key: '1',
|
||||
});
|
||||
});
|
||||
|
||||
test('parses a single page', () => {
|
||||
expect(parsePageSpec('7')).toEqual({
|
||||
pages: [7],
|
||||
key: '7',
|
||||
});
|
||||
});
|
||||
|
||||
test('parses a page range', () => {
|
||||
expect(parsePageSpec('1-3')).toEqual({
|
||||
pages: [1, 2, 3],
|
||||
key: '1,2,3',
|
||||
});
|
||||
});
|
||||
|
||||
test('parses a page list', () => {
|
||||
expect(parsePageSpec('1,3,5')).toEqual({
|
||||
pages: [1, 3, 5],
|
||||
key: '1,3,5',
|
||||
});
|
||||
});
|
||||
|
||||
test('parses a mixed page spec', () => {
|
||||
expect(parsePageSpec('1-3, 5')).toEqual({
|
||||
pages: [1, 2, 3, 5],
|
||||
key: '1,2,3,5',
|
||||
});
|
||||
});
|
||||
|
||||
test('rejects invalid page input', () => {
|
||||
expect(() => parsePageSpec('0')).toThrow(PageInvalidError);
|
||||
expect(() => parsePageSpec('3-1')).toThrow(PageInvalidError);
|
||||
expect(() => parsePageSpec('abc')).toThrow(PageInvalidError);
|
||||
expect(() => parsePageSpec('1,,3')).toThrow(PageInvalidError);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
export class PageInvalidError extends Error {
|
||||
constructor(page: string) {
|
||||
super(`page: ${page}`);
|
||||
}
|
||||
}
|
||||
|
||||
export interface PageSpec {
|
||||
pages: number[];
|
||||
key: string;
|
||||
}
|
||||
|
||||
const SINGLE_PAGE_PATTERN = /^[1-9]\d*$/;
|
||||
const PAGE_RANGE_PATTERN = /^([1-9]\d*)\s*-\s*([1-9]\d*)$/;
|
||||
|
||||
const normalizePages = (pages: number[]) => {
|
||||
const seen = new Set<number>();
|
||||
const normalized: number[] = [];
|
||||
|
||||
for (const page of pages) {
|
||||
if (seen.has(page)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(page);
|
||||
normalized.push(page);
|
||||
}
|
||||
|
||||
return normalized;
|
||||
};
|
||||
|
||||
const parsePageToken = (token: string) => {
|
||||
if (SINGLE_PAGE_PATTERN.test(token)) {
|
||||
return [Number(token)];
|
||||
}
|
||||
|
||||
const rangeMatch = token.match(PAGE_RANGE_PATTERN);
|
||||
if (rangeMatch) {
|
||||
const start = Number(rangeMatch[1]);
|
||||
const end = Number(rangeMatch[2]);
|
||||
if (end < start) {
|
||||
throw new PageInvalidError(token);
|
||||
}
|
||||
|
||||
return Array.from({ length: end - start + 1 }, (_, index) => start + index);
|
||||
}
|
||||
|
||||
throw new PageInvalidError(token);
|
||||
};
|
||||
|
||||
export const parsePageSpec = (page: unknown): PageSpec => {
|
||||
if (page === undefined || page === null) {
|
||||
return {
|
||||
pages: [1],
|
||||
key: '1',
|
||||
};
|
||||
}
|
||||
|
||||
if (Array.isArray(page)) {
|
||||
throw new PageInvalidError(page.join(','));
|
||||
}
|
||||
|
||||
const rawPage = String(page).trim();
|
||||
if (!rawPage) {
|
||||
return {
|
||||
pages: [1],
|
||||
key: '1',
|
||||
};
|
||||
}
|
||||
|
||||
const tokens = rawPage.split(',');
|
||||
if (tokens.some(token => !token.trim())) {
|
||||
throw new PageInvalidError(rawPage);
|
||||
}
|
||||
|
||||
const pages: number[] = [];
|
||||
for (const token of tokens) {
|
||||
const tokenPages = parsePageToken(token.trim());
|
||||
pages.push(...tokenPages);
|
||||
}
|
||||
|
||||
const normalizedPages = normalizePages(pages);
|
||||
if (!normalizedPages.length) {
|
||||
throw new PageInvalidError(rawPage);
|
||||
}
|
||||
|
||||
return {
|
||||
pages: normalizedPages,
|
||||
key: normalizedPages.join(','),
|
||||
};
|
||||
};
|
||||
+10
-2
@@ -8,6 +8,7 @@ import config from '../config';
|
||||
import { DomainNotFoundError, statusToHTML, UserNotFoundError, WeiboCookieInvalidError } from './weibo/weibo';
|
||||
import { ThrottledError } from './throttler';
|
||||
import { logger } from './logger';
|
||||
import { PageInvalidError, parsePageSpec } from './page';
|
||||
|
||||
export class UidInvalidError extends Error {
|
||||
constructor(uid: string) {
|
||||
@@ -32,10 +33,12 @@ export const registerRoutes = (
|
||||
throw new UidInvalidError(uid);
|
||||
}
|
||||
|
||||
const pageSpec = parsePageSpec(ctx.query.page);
|
||||
|
||||
// get data
|
||||
let cacheMiss = false;
|
||||
const xmlData = await ctx.cache.memo(async () => {
|
||||
const weiboData = await ctx.weibo.fetchUserLatestWeibo(uid);
|
||||
const weiboData = await ctx.weibo.fetchUserLatestWeibo(uid, pageSpec.pages);
|
||||
if (weiboData) {
|
||||
// basic info
|
||||
const feed = new NodeRSS({
|
||||
@@ -59,7 +62,7 @@ export const registerRoutes = (
|
||||
cacheMiss = true;
|
||||
return feed.xml();
|
||||
}
|
||||
}, `xml-${uid}`, config.cacheTTL.rssXml);
|
||||
}, `xml-${uid}-page-${pageSpec.key}`, config.cacheTTL.rssXml);
|
||||
|
||||
// send data
|
||||
ctx.set('Content-Type', 'text/xml');
|
||||
@@ -78,6 +81,11 @@ export const registerRoutes = (
|
||||
ctx.body = `找不到用户,可能用户仅登录可见,不支持订阅。可以通过打开 https://m.weibo.cn/u/:uid 验证(<a href="https://m.weibo.cn/u/${uid}" target="_blank">uid: ${uid}</a>)`;
|
||||
return;
|
||||
}
|
||||
if (error instanceof PageInvalidError) {
|
||||
ctx.status = 400;
|
||||
ctx.body = `page 参数格式有误。支持单页如 7,范围如 1-10,或列表如 1,3,5。page: ${error.message.replace(/^page:\s*/, '')}`;
|
||||
return;
|
||||
}
|
||||
if (error instanceof WeiboCookieInvalidError) {
|
||||
ctx.status = 401;
|
||||
ctx.body = `微博 Cookie 可能已过期或失效,请更新 WEIBO_COOKIE 后重试。uid: ${uid}`;
|
||||
|
||||
@@ -64,12 +64,12 @@ export const createIndexAPI = () => {
|
||||
return result;
|
||||
}).catch(err => handleForbiddenErr(err, disable));
|
||||
}),
|
||||
getWeiboContentList: (uid: string, containerId: string) => runner.runFunc<WeiboStatus[]>(async (disable) => {
|
||||
logger.debug(`[getContList] ${uid} ${containerId}`);
|
||||
getWeiboContentList: (uid: string, containerId: string, page: number = 1) => runner.runFunc<WeiboStatus[]>(async (disable) => {
|
||||
logger.debug(`[getContList] ${uid} ${containerId} page=${page}`);
|
||||
await waitMs(Math.floor(Math.random() * 100));
|
||||
return await axiosInstance({
|
||||
method: 'get',
|
||||
url: `https://m.weibo.cn/api/container/getIndex?type=uid&value=${uid}&containerid=${containerId}`,
|
||||
url: `https://m.weibo.cn/api/container/getIndex?type=uid&value=${uid}&containerid=${containerId}&page=${page}`,
|
||||
headers: {
|
||||
'Mweibo-Pwa': 1,
|
||||
'Referer': `https://m.weibo.cn/u/${uid}`,
|
||||
|
||||
@@ -42,15 +42,30 @@ export class WeiboData {
|
||||
/**
|
||||
* get user's weibo
|
||||
*/
|
||||
fetchUserLatestWeibo = async (uid: string) => {
|
||||
fetchUserLatestWeibo = async (uid: string, pages: number[] = [1]) => {
|
||||
const indexInfo = await this.cache.memo(() => this.getIndexUserInfo(uid), `info-${uid}`, config.cacheTTL.apiIndexInfo);
|
||||
const { containerId } = indexInfo;
|
||||
const statusList = await this.cache.memo(async () => {
|
||||
const wbList = await this.getWeiboContentList(uid, containerId);
|
||||
return await Promise.all(
|
||||
wbList.map(status => this.fillStatusWithLongText(status))
|
||||
const statusList: WeiboStatus[] = [];
|
||||
const seenStatusIds = new Set<string>();
|
||||
|
||||
for (const page of pages) {
|
||||
const wbList = await this.cache.memo(
|
||||
() => this.getWeiboContentList(uid, containerId, page),
|
||||
`list-${uid}-page-${page}`,
|
||||
config.cacheTTL.apiStatusList,
|
||||
);
|
||||
}, `list-${uid}`, config.cacheTTL.apiStatusList);
|
||||
const freshList = wbList.filter((status) => {
|
||||
if (seenStatusIds.has(status.id)) {
|
||||
return false;
|
||||
}
|
||||
seenStatusIds.add(status.id);
|
||||
return true;
|
||||
});
|
||||
const pageStatusList = await Promise.all(
|
||||
freshList.map(status => this.fillStatusWithLongText(status))
|
||||
);
|
||||
statusList.push(...pageStatusList);
|
||||
}
|
||||
|
||||
return {
|
||||
...indexInfo,
|
||||
|
||||
Reference in New Issue
Block a user