newsnow 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +130 -0
  2. package/dist/src/cli.js +104 -0
  3. package/dist/src/crypto.js +11 -0
  4. package/dist/src/fetch.js +36 -0
  5. package/dist/src/rss.js +25 -0
  6. package/dist/src/sources/_36kr.js +66 -0
  7. package/dist/src/sources/baidu.js +13 -0
  8. package/dist/src/sources/bilibili.js +52 -0
  9. package/dist/src/sources/cankaoxiaoxi.js +12 -0
  10. package/dist/src/sources/chongbuluo.js +28 -0
  11. package/dist/src/sources/cls/index.js +46 -0
  12. package/dist/src/sources/cls/utils.js +12 -0
  13. package/dist/src/sources/coolapk/index.js +44 -0
  14. package/dist/src/sources/douban.js +20 -0
  15. package/dist/src/sources/douyin.js +14 -0
  16. package/dist/src/sources/fastbull.js +52 -0
  17. package/dist/src/sources/freebuf.js +44 -0
  18. package/dist/src/sources/gelonghui.js +30 -0
  19. package/dist/src/sources/ghxi.js +48 -0
  20. package/dist/src/sources/github.js +29 -0
  21. package/dist/src/sources/hackernews.js +21 -0
  22. package/dist/src/sources/hupu.js +17 -0
  23. package/dist/src/sources/ifeng.js +21 -0
  24. package/dist/src/sources/index.js +90 -0
  25. package/dist/src/sources/iqiyi.js +17 -0
  26. package/dist/src/sources/ithome.js +29 -0
  27. package/dist/src/sources/jin10.js +24 -0
  28. package/dist/src/sources/juejin.js +11 -0
  29. package/dist/src/sources/kaopu.js +12 -0
  30. package/dist/src/sources/kuaishou.js +23 -0
  31. package/dist/src/sources/linuxdo.js +31 -0
  32. package/dist/src/sources/mktnews.js +20 -0
  33. package/dist/src/sources/nowcoder.js +19 -0
  34. package/dist/src/sources/pcbeta.js +5 -0
  35. package/dist/src/sources/producthunt.js +37 -0
  36. package/dist/src/sources/qqvideo.js +51 -0
  37. package/dist/src/sources/smzdm.js +17 -0
  38. package/dist/src/sources/solidot.js +27 -0
  39. package/dist/src/sources/sputniknewscn.js +25 -0
  40. package/dist/src/sources/sspai.js +13 -0
  41. package/dist/src/sources/steam.js +26 -0
  42. package/dist/src/sources/tencent.js +14 -0
  43. package/dist/src/sources/thepaper.js +12 -0
  44. package/dist/src/sources/tieba.js +11 -0
  45. package/dist/src/sources/toutiao.js +12 -0
  46. package/dist/src/sources/v2ex.js +14 -0
  47. package/dist/src/sources/wallstreetcn.js +38 -0
  48. package/dist/src/sources/weibo.js +46 -0
  49. package/dist/src/sources/xueqiu.js +18 -0
  50. package/dist/src/sources/zaobao.js +31 -0
  51. package/dist/src/sources/zhihu.js +15 -0
  52. package/dist/src/types.js +1 -0
  53. package/dist/src/utils.js +46 -0
  54. package/dist/test/cli.test.js +61 -0
  55. package/package.json +28 -0
@@ -0,0 +1,25 @@
1
+ import * as cheerio from "cheerio";
2
+ import { myFetch } from "../fetch.js";
3
+ async function handler() {
4
+ const response = await myFetch("https://sputniknews.cn/services/widget/lenta/");
5
+ const $ = cheerio.load(response);
6
+ const $items = $(".lenta__item");
7
+ const news = [];
8
+ $items.each((_, el) => {
9
+ const $el = $(el);
10
+ const $a = $el.find("a");
11
+ const url = $a.attr("href");
12
+ const title = $a.find(".lenta__item-text").text();
13
+ const date = $a.find(".lenta__item-date").attr("data-unixtime");
14
+ if (url && title && date) {
15
+ news.push({
16
+ url: `https://sputniknews.cn${url}`,
17
+ title,
18
+ id: url,
19
+ extra: { date: new Date(Number(`${date}000`)).getTime() },
20
+ });
21
+ }
22
+ });
23
+ return news;
24
+ }
25
+ export default { sputniknewscn: handler };
@@ -0,0 +1,13 @@
1
+ import { myFetch } from "../fetch.js";
2
+ async function handler() {
3
+ const timestamp = Date.now();
4
+ const limit = 30;
5
+ const url = `https://sspai.com/api/v1/article/tag/page/get?limit=${limit}&offset=0&created_at=${timestamp}&tag=%E7%83%AD%E9%97%A8%E6%96%87%E7%AB%A0&released=false`;
6
+ const res = await myFetch(url);
7
+ return res.data.map((k) => ({
8
+ id: k.id,
9
+ title: k.title,
10
+ url: `https://sspai.com/post/${k.id}`,
11
+ }));
12
+ }
13
+ export default { sspai: handler };
@@ -0,0 +1,26 @@
1
+ import * as cheerio from "cheerio";
2
+ import { myFetch } from "../fetch.js";
3
+ async function handler() {
4
+ const response = await myFetch("https://store.steampowered.com/stats/stats/");
5
+ const $ = cheerio.load(response);
6
+ const $rows = $("#detailStats tr.player_count_row");
7
+ const news = [];
8
+ $rows.each((_, el) => {
9
+ const $el = $(el);
10
+ const $a = $el.find("a.gameLink");
11
+ const url = $a.attr("href");
12
+ const gameName = $a.text().trim();
13
+ const currentPlayers = $el.find("td:first-child .currentServers").text().trim();
14
+ if (url && gameName && currentPlayers) {
15
+ news.push({
16
+ url,
17
+ title: gameName,
18
+ id: url,
19
+ pubDate: Date.now(),
20
+ extra: { info: currentPlayers },
21
+ });
22
+ }
23
+ });
24
+ return news;
25
+ }
26
+ export default { steam: handler };
@@ -0,0 +1,14 @@
1
+ import { myFetch } from "../fetch.js";
2
+ const comprehensiveNews = async () => {
3
+ const url = "https://i.news.qq.com/web_backend/v2/getTagInfo?tagId=aEWqxLtdgmQ%3D";
4
+ const res = await myFetch(url, {
5
+ headers: { Referer: "https://news.qq.com/" },
6
+ });
7
+ return res.data.tabs[0].articleList.map(news => ({
8
+ id: news.id,
9
+ title: news.title,
10
+ url: news.link_info.url,
11
+ extra: { hover: news.desc },
12
+ }));
13
+ };
14
+ export default { "tencent-hot": comprehensiveNews };
@@ -0,0 +1,12 @@
1
+ import { myFetch } from "../fetch.js";
2
+ async function handler() {
3
+ const url = "https://cache.thepaper.cn/contentapi/wwwIndex/rightSidebar";
4
+ const res = await myFetch(url);
5
+ return res.data.hotNews.map((k) => ({
6
+ id: k.contId,
7
+ title: k.name,
8
+ url: `https://www.thepaper.cn/newsDetail_forward_${k.contId}`,
9
+ mobileUrl: `https://m.thepaper.cn/newsDetail_forward_${k.contId}`,
10
+ }));
11
+ }
12
+ export default { thepaper: handler };
@@ -0,0 +1,11 @@
1
+ import { myFetch } from "../fetch.js";
2
+ async function handler() {
3
+ const url = "https://tieba.baidu.com/hottopic/browse/topicList";
4
+ const res = await myFetch(url);
5
+ return res.data.bang_topic.topic_list.map((k) => ({
6
+ id: k.topic_id,
7
+ title: k.topic_name,
8
+ url: k.topic_url,
9
+ }));
10
+ }
11
+ export default { tieba: handler };
@@ -0,0 +1,12 @@
1
+ import { myFetch } from "../fetch.js";
2
+ async function handler() {
3
+ const url = "https://www.toutiao.com/hot-event/hot-board/?origin=toutiao_pc";
4
+ const res = await myFetch(url);
5
+ return res.data.map((k) => ({
6
+ id: k.ClusterIdStr,
7
+ title: k.Title,
8
+ url: `https://www.toutiao.com/trending/${k.ClusterIdStr}/`,
9
+ extra: { icon: k.LabelUri?.url },
10
+ }));
11
+ }
12
+ export default { toutiao: handler };
@@ -0,0 +1,14 @@
1
+ import { myFetch } from "../fetch.js";
2
+ const share = async () => {
3
+ const res = await Promise.all(["create", "ideas", "programmer", "share"].map(k => myFetch(`https://www.v2ex.com/feed/${k}.json`)));
4
+ return res.map(k => k.items).flat().map(k => ({
5
+ id: k.id,
6
+ title: k.title,
7
+ extra: { date: k.date_modified ?? k.date_published },
8
+ url: k.url,
9
+ })).sort((m, n) => m.extra.date < n.extra.date ? 1 : -1);
10
+ };
11
+ export default {
12
+ "v2ex": share,
13
+ "v2ex-share": share,
14
+ };
@@ -0,0 +1,38 @@
1
+ import { myFetch } from "../fetch.js";
2
+ const live = async () => {
3
+ const apiUrl = `https://api-one.wallstcn.com/apiv1/content/lives?channel=global-channel&limit=30`;
4
+ const res = await myFetch(apiUrl);
5
+ return res.data.items.map((k) => ({
6
+ id: k.id,
7
+ title: k.title || k.content_text,
8
+ extra: { date: k.display_time * 1000 },
9
+ url: k.uri,
10
+ }));
11
+ };
12
+ const news = async () => {
13
+ const apiUrl = `https://api-one.wallstcn.com/apiv1/content/information-flow?channel=global-channel&accept=article&limit=30`;
14
+ const res = await myFetch(apiUrl);
15
+ return res.data.items
16
+ .filter((k) => k.resource_type !== "theme" && k.resource_type !== "ad" && k.resource.type !== "live" && k.resource.uri)
17
+ .map(({ resource: h }) => ({
18
+ id: h.id,
19
+ title: h.title || h.content_short,
20
+ extra: { date: h.display_time * 1000 },
21
+ url: h.uri,
22
+ }));
23
+ };
24
+ const hot = async () => {
25
+ const apiUrl = `https://api-one.wallstcn.com/apiv1/content/articles/hot?period=all`;
26
+ const res = await myFetch(apiUrl);
27
+ return res.data.day_items.map((h) => ({
28
+ id: h.id,
29
+ title: h.title,
30
+ url: h.uri,
31
+ }));
32
+ };
33
+ export default {
34
+ "wallstreetcn": live,
35
+ "wallstreetcn-quick": live,
36
+ "wallstreetcn-news": news,
37
+ "wallstreetcn-hot": hot,
38
+ };
@@ -0,0 +1,46 @@
1
+ import * as cheerio from "cheerio";
2
+ import { myFetch } from "../fetch.js";
3
+ async function handler() {
4
+ const baseurl = "https://s.weibo.com";
5
+ const url = `${baseurl}/top/summary?cate=realtimehot`;
6
+ const html = await myFetch(url, {
7
+ headers: {
8
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
9
+ "Cookie": "SUB=_2AkMWIuNSf8NxqwJRmP8dy2rhaoV2ygrEieKgfhKJJRMxHRl-yT9jqk86tRB6PaLNvQZR6zYUcYVT1zSjoSreQHidcUq7",
10
+ "referer": url,
11
+ },
12
+ });
13
+ const $ = cheerio.load(html);
14
+ const rows = $("#pl_top_realtimehot table tbody tr").slice(1);
15
+ const hotNews = [];
16
+ rows.each((_, row) => {
17
+ const $row = $(row);
18
+ const $link = $row.find("td.td-02 a").filter((_, el) => {
19
+ const href = $(el).attr("href");
20
+ return !!(href && !href.includes("javascript:void(0);"));
21
+ }).first();
22
+ if ($link.length) {
23
+ const title = $link.text().trim();
24
+ const href = $link.attr("href");
25
+ if (title && href) {
26
+ const $flag = $row.find("td.td-03").text().trim();
27
+ const flagUrl = {
28
+ "新": "https://simg.s.weibo.com/moter/flags/1_0.png",
29
+ "热": "https://simg.s.weibo.com/moter/flags/2_0.png",
30
+ "爆": "https://simg.s.weibo.com/moter/flags/4_0.png",
31
+ };
32
+ hotNews.push({
33
+ id: title,
34
+ title,
35
+ url: `${baseurl}${href}`,
36
+ mobileUrl: `${baseurl}${href}`,
37
+ extra: {
38
+ icon: flagUrl[$flag] ? { url: flagUrl[$flag], scale: 1.5 } : undefined,
39
+ },
40
+ });
41
+ }
42
+ }
43
+ });
44
+ return hotNews;
45
+ }
46
+ export default { weibo: handler };
@@ -0,0 +1,18 @@
1
+ import { myFetch, $fetch } from "../fetch.js";
2
+ const hotstock = async () => {
3
+ const cookie = (await $fetch.raw("https://xueqiu.com/hq")).headers.getSetCookie();
4
+ const url = "https://stock.xueqiu.com/v5/stock/hot_stock/list.json?size=30&_type=10&type=10";
5
+ const res = await myFetch(url, {
6
+ headers: { cookie: cookie.join("; ") },
7
+ });
8
+ return res.data.items.filter(k => !k.ad).map(k => ({
9
+ id: k.code,
10
+ url: `https://xueqiu.com/s/${k.code}`,
11
+ title: k.name,
12
+ extra: { info: `${k.percent}% ${k.exchange}` },
13
+ }));
14
+ };
15
+ export default {
16
+ "xueqiu": hotstock,
17
+ "xueqiu-hotstock": hotstock,
18
+ };
@@ -0,0 +1,31 @@
1
+ import { Buffer } from "node:buffer";
2
+ import * as cheerio from "cheerio";
3
+ import iconv from "iconv-lite";
4
+ import { myFetch } from "../fetch.js";
5
+ import { parseRelativeDate } from "../utils.js";
6
+ async function handler() {
7
+ const response = await myFetch("https://www.zaochenbao.com/realtime/", {
8
+ responseType: "arrayBuffer",
9
+ });
10
+ const base = "https://www.zaochenbao.com";
11
+ const utf8String = iconv.decode(Buffer.from(response), "gb2312");
12
+ const $ = cheerio.load(utf8String);
13
+ const $main = $("div.list-block>a.item");
14
+ const news = [];
15
+ $main.each((_, el) => {
16
+ const a = $(el);
17
+ const url = a.attr("href");
18
+ const title = a.find(".eps")?.text();
19
+ const date = a.find(".pdt10")?.text().replace(/-\s/g, " ");
20
+ if (url && title && date) {
21
+ news.push({
22
+ url: base + url,
23
+ title,
24
+ id: url,
25
+ pubDate: parseRelativeDate(date, "Asia/Shanghai").valueOf(),
26
+ });
27
+ }
28
+ });
29
+ return news.sort((m, n) => n.pubDate > m.pubDate ? 1 : -1);
30
+ }
31
+ export default { zaobao: handler };
@@ -0,0 +1,15 @@
1
+ import { myFetch } from "../fetch.js";
2
+ const handler = async () => {
3
+ const url = "https://www.zhihu.com/api/v3/feed/topstory/hot-list-web?limit=20&desktop=true";
4
+ const res = await myFetch(url);
5
+ return res.data.map((k) => ({
6
+ id: k.target.link.url.match(/(\d+)$/)?.[1] ?? k.target.link.url,
7
+ title: k.target.title_area.text,
8
+ extra: {
9
+ info: k.target.metrics_area.text,
10
+ hover: k.target.excerpt_area.text,
11
+ },
12
+ url: k.target.link.url,
13
+ }));
14
+ };
15
+ export default { zhihu: handler };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,46 @@
1
+ export function parseRelativeDate(dateStr, _timezone) {
2
+ const now = new Date();
3
+ const relativeMatch = dateStr.match(/(\d+)\s*(秒|分钟|小时|天|周|月|年)前/);
4
+ if (relativeMatch) {
5
+ const num = parseInt(relativeMatch[1]);
6
+ const unit = relativeMatch[2];
7
+ const units = {
8
+ "秒": 1000,
9
+ "分钟": 60 * 1000,
10
+ "小时": 60 * 60 * 1000,
11
+ "天": 24 * 60 * 60 * 1000,
12
+ "周": 7 * 24 * 60 * 60 * 1000,
13
+ "月": 30 * 24 * 60 * 60 * 1000,
14
+ "年": 365 * 24 * 60 * 60 * 1000,
15
+ };
16
+ if (units[unit]) {
17
+ return new Date(now.getTime() - num * units[unit]);
18
+ }
19
+ }
20
+ const dateOnly = dateStr.match(/^(\d{4})-(\d{1,2})-(\d{1,2})\s+(\d{1,2}):(\d{1,2})$/);
21
+ if (dateOnly) {
22
+ return new Date(`${dateOnly[1]}-${dateOnly[2].padStart(2, "0")}-${dateOnly[3].padStart(2, "0")}T${dateOnly[4].padStart(2, "0")}:${dateOnly[5].padStart(2, "0")}:00+08:00`);
23
+ }
24
+ const todayMatch = dateStr.match(/今天\s*(\d{1,2}):(\d{1,2})/);
25
+ if (todayMatch) {
26
+ const d = new Date(now);
27
+ d.setHours(parseInt(todayMatch[1]), parseInt(todayMatch[2]), 0, 0);
28
+ return d;
29
+ }
30
+ const yesterdayMatch = dateStr.match(/昨天\s*(\d{1,2}):(\d{1,2})/);
31
+ if (yesterdayMatch) {
32
+ const d = new Date(now.getTime() - 24 * 60 * 60 * 1000);
33
+ d.setHours(parseInt(yesterdayMatch[1]), parseInt(yesterdayMatch[2]), 0, 0);
34
+ return d;
35
+ }
36
+ const parsed = new Date(dateStr);
37
+ if (!isNaN(parsed.getTime()))
38
+ return parsed;
39
+ return now;
40
+ }
41
+ export function tranformToUTC(dateStr) {
42
+ const d = new Date(dateStr);
43
+ if (!isNaN(d.getTime()))
44
+ return d.getTime();
45
+ return Date.now();
46
+ }
@@ -0,0 +1,61 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { sources } from "../src/sources/index.js";
3
+ describe("registry", () => {
4
+ test("has sources registered", () => {
5
+ const names = Object.keys(sources);
6
+ expect(names.length).toBeGreaterThan(40);
7
+ });
8
+ test("all sources are functions", () => {
9
+ for (const [name, handler] of Object.entries(sources)) {
10
+ expect(typeof handler).toBe("function");
11
+ }
12
+ });
13
+ test("contains expected source names", () => {
14
+ const expected = [
15
+ "baidu", "bilibili", "hackernews", "github", "weibo",
16
+ "zhihu", "v2ex", "juejin", "36kr", "toutiao",
17
+ ];
18
+ for (const name of expected) {
19
+ expect(sources).toHaveProperty(name);
20
+ }
21
+ });
22
+ });
23
+ describe("cli", () => {
24
+ test("list command outputs sources", async () => {
25
+ const proc = Bun.spawn(["bun", "src/cli.ts", "list", "--json"], {
26
+ cwd: import.meta.dir + "/..",
27
+ stdout: "pipe",
28
+ });
29
+ const output = await new Response(proc.stdout).text();
30
+ const names = JSON.parse(output);
31
+ expect(Array.isArray(names)).toBe(true);
32
+ expect(names.length).toBeGreaterThan(40);
33
+ expect(names).toContain("hackernews");
34
+ });
35
+ test("help command works", async () => {
36
+ const proc = Bun.spawn(["bun", "src/cli.ts", "--help"], {
37
+ cwd: import.meta.dir + "/..",
38
+ stdout: "pipe",
39
+ });
40
+ const output = await new Response(proc.stdout).text();
41
+ expect(output).toContain("Usage:");
42
+ });
43
+ test("unknown source shows error", async () => {
44
+ const proc = Bun.spawn(["bun", "src/cli.ts", "nonexistent_xyz"], {
45
+ cwd: import.meta.dir + "/..",
46
+ stderr: "pipe",
47
+ });
48
+ const err = await new Response(proc.stderr).text();
49
+ expect(err).toContain("Unknown source");
50
+ });
51
+ });
52
+ describe("fetch source", () => {
53
+ test("hackernews returns items", async () => {
54
+ const handler = sources["hackernews"];
55
+ const items = await handler();
56
+ expect(items.length).toBeGreaterThan(0);
57
+ expect(items[0]).toHaveProperty("title");
58
+ expect(items[0]).toHaveProperty("id");
59
+ expect(items[0]).toHaveProperty("url");
60
+ }, 15000);
61
+ });
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "newsnow",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "newsnow": "./dist/src/cli.js"
7
+ },
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "scripts": {
12
+ "start": "bun src/cli.ts",
13
+ "test": "bun test",
14
+ "build": "tsc",
15
+ "release": "bun run build && npm publish"
16
+ },
17
+ "dependencies": {
18
+ "cheerio": "^1.0.0",
19
+ "dayjs": "^1.11.0",
20
+ "iconv-lite": "^0.6.3",
21
+ "ofetch": "^1.4.0"
22
+ },
23
+ "devDependencies": {
24
+ "@types/bun": "latest",
25
+ "@types/node": "^22.0.0",
26
+ "typescript": "^5.7.0"
27
+ }
28
+ }