diff --git a/README.md b/README.md
index 7cff49a..aecbbd9 100644
--- a/README.md
+++ b/README.md
@@ -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编码及百度代理
diff --git a/public/index.html b/public/index.html
index cbb522c..3fc3048 100644
--- a/public/index.html
+++ b/public/index.html
@@ -52,6 +52,10 @@
+
+ 页码
+
+
@@ -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) {
diff --git a/src/modules/page.spec.ts b/src/modules/page.spec.ts
new file mode 100644
index 0000000..1b61592
--- /dev/null
+++ b/src/modules/page.spec.ts
@@ -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);
+ });
+});
diff --git a/src/modules/page.ts b/src/modules/page.ts
new file mode 100644
index 0000000..7faa35d
--- /dev/null
+++ b/src/modules/page.ts
@@ -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();
+ 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(','),
+ };
+};
diff --git a/src/modules/routes.ts b/src/modules/routes.ts
index 7af095e..f76321b 100644
--- a/src/modules/routes.ts
+++ b/src/modules/routes.ts
@@ -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 验证(uid: ${uid})`;
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}`;
diff --git a/src/modules/weibo/api/indexAPI.ts b/src/modules/weibo/api/indexAPI.ts
index b857531..d191b4a 100644
--- a/src/modules/weibo/api/indexAPI.ts
+++ b/src/modules/weibo/api/indexAPI.ts
@@ -64,12 +64,12 @@ export const createIndexAPI = () => {
return result;
}).catch(err => handleForbiddenErr(err, disable));
}),
- getWeiboContentList: (uid: string, containerId: string) => runner.runFunc(async (disable) => {
- logger.debug(`[getContList] ${uid} ${containerId}`);
+ getWeiboContentList: (uid: string, containerId: string, page: number = 1) => runner.runFunc(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}`,
diff --git a/src/modules/weibo/weibo.ts b/src/modules/weibo/weibo.ts
index 315a179..a656d9f 100644
--- a/src/modules/weibo/weibo.ts
+++ b/src/modules/weibo/weibo.ts
@@ -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();
+
+ 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,