diff --git a/README.zh-CN.md b/README.zh-CN.md index 091e9fb8f..44d334ea1 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -211,7 +211,7 @@ npm link | **xiaoyuzhou** | `podcast` `podcast-episodes` `episode` | 公开 | | **zhihu** | `hot` `search` `question` `download` `follow` `like` `favorite` `comment` `answer` | 浏览器 | | **weixin** | `download` | 浏览器 | -| **youtube** | `search` `video` `transcript` | 浏览器 | +| **youtube** | `search` `video` `transcript` `comments` `channel` `playlist` `feed` `history` `watch-later` `subscriptions` `like` `unlike` `subscribe` `unsubscribe` | 浏览器 | | **boss** | `search` `detail` `recommend` `joblist` `greet` `batchgreet` `send` `chatlist` `chatmsg` `invite` `mark` `exchange` `resume` `stats` | 浏览器 | | **coupang** | `search` `add-to-cart` | 浏览器 | | **bbc** | `news` | 公共 API | diff --git a/cli-manifest.json b/cli-manifest.json index 48b6769de..65600e6bb 100644 --- a/cli-manifest.json +++ b/cli-manifest.json @@ -25,7 +25,8 @@ ], "type": "js", "modulePath": "1688/assets.js", - "sourceFile": "1688/assets.js" + "sourceFile": "1688/assets.js", + "navigateBefore": "https://www.1688.com" }, { "site": "1688", @@ -58,7 +59,8 @@ ], "type": "js", "modulePath": "1688/download.js", - "sourceFile": "1688/download.js" + "sourceFile": "1688/download.js", + "navigateBefore": "https://www.1688.com" }, { "site": "1688", @@ -174,7 +176,8 @@ ], "type": "js", "modulePath": "36kr/article.js", - "sourceFile": "36kr/article.js" + "sourceFile": "36kr/article.js", + "navigateBefore": true }, { "site": "36kr", @@ -519,7 +522,8 @@ ], "type": "js", "modulePath": "antigravity/dump.js", - "sourceFile": "antigravity/dump.js" + "sourceFile": "antigravity/dump.js", + "navigateBefore": true }, { "site": "antigravity", @@ -534,7 +538,8 @@ ], "type": "js", "modulePath": "antigravity/extract-code.js", - "sourceFile": "antigravity/extract-code.js" + "sourceFile": "antigravity/extract-code.js", + "navigateBefore": true }, { "site": "antigravity", @@ -557,7 +562,8 @@ ], "type": "js", "modulePath": "antigravity/model.js", - "sourceFile": "antigravity/model.js" + "sourceFile": "antigravity/model.js", + "navigateBefore": true }, { "site": "antigravity", @@ -572,7 +578,8 @@ ], "type": "js", "modulePath": "antigravity/new.js", - "sourceFile": "antigravity/new.js" + "sourceFile": "antigravity/new.js", + "navigateBefore": true }, { "site": "antigravity", @@ -595,7 +602,8 @@ ], "type": "js", "modulePath": "antigravity/read.js", - "sourceFile": "antigravity/read.js" + "sourceFile": "antigravity/read.js", + "navigateBefore": true }, { "site": "antigravity", @@ -619,7 +627,8 @@ ], "type": "js", "modulePath": "antigravity/send.js", - "sourceFile": "antigravity/send.js" + "sourceFile": "antigravity/send.js", + "navigateBefore": true }, { "site": "antigravity", @@ -636,7 +645,8 @@ ], "type": "js", "modulePath": "antigravity/status.js", - "sourceFile": "antigravity/status.js" + "sourceFile": "antigravity/status.js", + "navigateBefore": true }, { "site": "antigravity", @@ -650,7 +660,8 @@ "timeout": 86400, "type": "js", "modulePath": "antigravity/watch.js", - "sourceFile": "antigravity/watch.js" + "sourceFile": "antigravity/watch.js", + "navigateBefore": true }, { "site": "apple-podcasts", @@ -824,7 +835,8 @@ ], "type": "js", "modulePath": "band/bands.js", - "sourceFile": "band/bands.js" + "sourceFile": "band/bands.js", + "navigateBefore": "https://www.band.us" }, { "site": "band", @@ -872,7 +884,8 @@ ], "type": "js", "modulePath": "band/mentions.js", - "sourceFile": "band/mentions.js" + "sourceFile": "band/mentions.js", + "navigateBefore": true }, { "site": "band", @@ -998,7 +1011,8 @@ ], "type": "js", "modulePath": "barchart/flow.js", - "sourceFile": "barchart/flow.js" + "sourceFile": "barchart/flow.js", + "navigateBefore": "https://www.barchart.com" }, { "site": "barchart", @@ -1045,7 +1059,8 @@ ], "type": "js", "modulePath": "barchart/greeks.js", - "sourceFile": "barchart/greeks.js" + "sourceFile": "barchart/greeks.js", + "navigateBefore": "https://www.barchart.com" }, { "site": "barchart", @@ -1098,7 +1113,8 @@ ], "type": "js", "modulePath": "barchart/options.js", - "sourceFile": "barchart/options.js" + "sourceFile": "barchart/options.js", + "navigateBefore": "https://www.barchart.com" }, { "site": "barchart", @@ -1134,7 +1150,8 @@ ], "type": "js", "modulePath": "barchart/quote.js", - "sourceFile": "barchart/quote.js" + "sourceFile": "barchart/quote.js", + "navigateBefore": "https://www.barchart.com" }, { "site": "bbc", @@ -1195,7 +1212,8 @@ ], "type": "js", "modulePath": "bilibili/comments.js", - "sourceFile": "bilibili/comments.js" + "sourceFile": "bilibili/comments.js", + "navigateBefore": "https://www.bilibili.com" }, { "site": "bilibili", @@ -1235,7 +1253,8 @@ ], "type": "js", "modulePath": "bilibili/download.js", - "sourceFile": "bilibili/download.js" + "sourceFile": "bilibili/download.js", + "navigateBefore": "https://www.bilibili.com" }, { "site": "bilibili", @@ -1262,7 +1281,8 @@ ], "type": "js", "modulePath": "bilibili/dynamic.js", - "sourceFile": "bilibili/dynamic.js" + "sourceFile": "bilibili/dynamic.js", + "navigateBefore": "https://www.bilibili.com" }, { "site": "bilibili", @@ -1302,46 +1322,90 @@ ], "type": "js", "modulePath": "bilibili/favorite.js", - "sourceFile": "bilibili/favorite.js" + "sourceFile": "bilibili/favorite.js", + "navigateBefore": "https://www.bilibili.com" }, { "site": "bilibili", "name": "feed", - "description": "关注的人的动态时间线", + "description": "动态时间线(不传 uid 查关注时间线,传 uid 查指定用户动态)", "domain": "www.bilibili.com", "strategy": "cookie", "browser": true, "args": [ + { + "name": "uid", + "type": "str", + "required": false, + "positional": true, + "help": "用户 UID 或用户名(不传则显示关注时间线)" + }, { "name": "limit", "type": "int", "default": 20, "required": false, - "help": "Number of results" + "help": "Max results to return" }, { "name": "type", "type": "str", "default": "all", "required": false, - "help": "Filter: all, video, article" + "help": "Filter: all, video, article, draw, text" + }, + { + "name": "pages", + "type": "int", + "default": 1, + "required": false, + "help": "Number of pages to fetch (each ~20 items)" } ], "columns": [ "rank", + "time", "author", "title", "type", + "likes", "url" ], "type": "js", "modulePath": "bilibili/feed.js", - "sourceFile": "bilibili/feed.js" + "sourceFile": "bilibili/feed.js", + "navigateBefore": "https://www.bilibili.com" + }, + { + "site": "bilibili", + "name": "feed-detail", + "description": "查看 Bilibili 动态详情(支持充电专属内容)", + "domain": "www.bilibili.com", + "strategy": "cookie", + "browser": true, + "args": [ + { + "name": "id", + "type": "str", + "required": true, + "positional": true, + "help": "动态 ID(从 feed 命令的 url 中获取)" + } + ], + "columns": [ + "field", + "value" + ], + "type": "js", + "modulePath": "bilibili/feed.js", + "sourceFile": "bilibili/feed.js", + "navigateBefore": "https://www.bilibili.com" }, { "site": "bilibili", "name": "following", "description": "获取 Bilibili 用户的关注列表", + "domain": "www.bilibili.com", "strategy": "cookie", "browser": true, "args": [ @@ -1376,7 +1440,8 @@ ], "type": "js", "modulePath": "bilibili/following.js", - "sourceFile": "bilibili/following.js" + "sourceFile": "bilibili/following.js", + "navigateBefore": "https://www.bilibili.com" }, { "site": "bilibili", @@ -1403,7 +1468,8 @@ ], "type": "js", "modulePath": "bilibili/history.js", - "sourceFile": "bilibili/history.js" + "sourceFile": "bilibili/history.js", + "navigateBefore": "https://www.bilibili.com" }, { "site": "bilibili", @@ -1430,7 +1496,8 @@ ], "type": "js", "modulePath": "bilibili/hot.js", - "sourceFile": "bilibili/hot.js" + "sourceFile": "bilibili/hot.js", + "navigateBefore": "https://www.bilibili.com" }, { "site": "bilibili", @@ -1450,7 +1517,8 @@ ], "type": "js", "modulePath": "bilibili/me.js", - "sourceFile": "bilibili/me.js" + "sourceFile": "bilibili/me.js", + "navigateBefore": "https://www.bilibili.com" }, { "site": "bilibili", @@ -1477,7 +1545,8 @@ ], "type": "js", "modulePath": "bilibili/ranking.js", - "sourceFile": "bilibili/ranking.js" + "sourceFile": "bilibili/ranking.js", + "navigateBefore": "https://www.bilibili.com" }, { "site": "bilibili", @@ -1525,7 +1594,8 @@ ], "type": "js", "modulePath": "bilibili/search.js", - "sourceFile": "bilibili/search.js" + "sourceFile": "bilibili/search.js", + "navigateBefore": "https://www.bilibili.com" }, { "site": "bilibili", @@ -1556,7 +1626,8 @@ ], "type": "js", "modulePath": "bilibili/subtitle.js", - "sourceFile": "bilibili/subtitle.js" + "sourceFile": "bilibili/subtitle.js", + "navigateBefore": true }, { "site": "bilibili", @@ -1605,7 +1676,342 @@ ], "type": "js", "modulePath": "bilibili/user-videos.js", - "sourceFile": "bilibili/user-videos.js" + "sourceFile": "bilibili/user-videos.js", + "navigateBefore": "https://www.bilibili.com" + }, + { + "site": "binance", + "name": "asks", + "description": "Order book ask prices for a trading pair", + "domain": "data-api.binance.vision", + "strategy": "public", + "browser": false, + "args": [ + { + "name": "symbol", + "type": "str", + "required": true, + "positional": true, + "help": "Trading pair symbol (e.g. BTCUSDT, ETHUSDT)" + }, + { + "name": "limit", + "type": "int", + "default": 10, + "required": false, + "help": "Number of price levels (5, 10, 20, 50, 100)" + } + ], + "columns": [ + "rank", + "ask_price", + "ask_qty" + ], + "type": "js", + "modulePath": "binance/asks.js", + "sourceFile": "binance/asks.js" + }, + { + "site": "binance", + "name": "depth", + "description": "Order book bid prices for a trading pair", + "domain": "data-api.binance.vision", + "strategy": "public", + "browser": false, + "args": [ + { + "name": "symbol", + "type": "str", + "required": true, + "positional": true, + "help": "Trading pair symbol (e.g. BTCUSDT, ETHUSDT)" + }, + { + "name": "limit", + "type": "int", + "default": 10, + "required": false, + "help": "Number of price levels (5, 10, 20, 50, 100)" + } + ], + "columns": [ + "rank", + "bid_price", + "bid_qty" + ], + "type": "js", + "modulePath": "binance/depth.js", + "sourceFile": "binance/depth.js" + }, + { + "site": "binance", + "name": "gainers", + "description": "Top gaining trading pairs by 24h price change", + "domain": "data-api.binance.vision", + "strategy": "public", + "browser": false, + "args": [ + { + "name": "limit", + "type": "int", + "default": 10, + "required": false, + "help": "Number of trading pairs" + } + ], + "columns": [ + "rank", + "symbol", + "price", + "change_24h", + "volume" + ], + "type": "js", + "modulePath": "binance/gainers.js", + "sourceFile": "binance/gainers.js" + }, + { + "site": "binance", + "name": "klines", + "description": "Candlestick/kline data for a trading pair", + "domain": "data-api.binance.vision", + "strategy": "public", + "browser": false, + "args": [ + { + "name": "symbol", + "type": "str", + "required": true, + "positional": true, + "help": "Trading pair symbol (e.g. BTCUSDT, ETHUSDT)" + }, + { + "name": "interval", + "type": "str", + "default": "1d", + "required": false, + "help": "Kline interval (1m, 5m, 15m, 1h, 4h, 1d, 1w, 1M)" + }, + { + "name": "limit", + "type": "int", + "default": 10, + "required": false, + "help": "Number of klines (max 1000)" + } + ], + "columns": [ + "open", + "high", + "low", + "close", + "volume" + ], + "type": "js", + "modulePath": "binance/klines.js", + "sourceFile": "binance/klines.js" + }, + { + "site": "binance", + "name": "losers", + "description": "Top losing trading pairs by 24h price change", + "domain": "data-api.binance.vision", + "strategy": "public", + "browser": false, + "args": [ + { + "name": "limit", + "type": "int", + "default": 10, + "required": false, + "help": "Number of trading pairs" + } + ], + "columns": [ + "rank", + "symbol", + "price", + "change_24h", + "volume" + ], + "type": "js", + "modulePath": "binance/losers.js", + "sourceFile": "binance/losers.js" + }, + { + "site": "binance", + "name": "pairs", + "description": "List active trading pairs on Binance", + "domain": "data-api.binance.vision", + "strategy": "public", + "browser": false, + "args": [ + { + "name": "limit", + "type": "int", + "default": 20, + "required": false, + "help": "Number of trading pairs" + } + ], + "columns": [ + "symbol", + "base", + "quote", + "status" + ], + "type": "js", + "modulePath": "binance/pairs.js", + "sourceFile": "binance/pairs.js" + }, + { + "site": "binance", + "name": "price", + "description": "Quick price check for a trading pair", + "domain": "data-api.binance.vision", + "strategy": "public", + "browser": false, + "args": [ + { + "name": "symbol", + "type": "str", + "required": true, + "positional": true, + "help": "Trading pair symbol (e.g. BTCUSDT, ETHUSDT)" + } + ], + "columns": [ + "symbol", + "price", + "change", + "change_pct", + "high", + "low", + "volume", + "quote_volume", + "trades" + ], + "type": "js", + "modulePath": "binance/price.js", + "sourceFile": "binance/price.js" + }, + { + "site": "binance", + "name": "prices", + "description": "Latest prices for all trading pairs", + "domain": "data-api.binance.vision", + "strategy": "public", + "browser": false, + "args": [ + { + "name": "limit", + "type": "int", + "default": 20, + "required": false, + "help": "Number of prices" + } + ], + "columns": [ + "rank", + "symbol", + "price" + ], + "type": "js", + "modulePath": "binance/prices.js", + "sourceFile": "binance/prices.js" + }, + { + "site": "binance", + "name": "ticker", + "description": "24h ticker statistics for top trading pairs by volume", + "domain": "data-api.binance.vision", + "strategy": "public", + "browser": false, + "args": [ + { + "name": "limit", + "type": "int", + "default": 20, + "required": false, + "help": "Number of tickers" + } + ], + "columns": [ + "symbol", + "price", + "change_pct", + "high", + "low", + "volume", + "quote_vol", + "trades" + ], + "type": "js", + "modulePath": "binance/ticker.js", + "sourceFile": "binance/ticker.js" + }, + { + "site": "binance", + "name": "top", + "description": "Top trading pairs by 24h volume on Binance", + "domain": "data-api.binance.vision", + "strategy": "public", + "browser": false, + "args": [ + { + "name": "limit", + "type": "int", + "default": 20, + "required": false, + "help": "Number of trading pairs" + } + ], + "columns": [ + "rank", + "symbol", + "price", + "change_24h", + "high", + "low", + "volume" + ], + "type": "js", + "modulePath": "binance/top.js", + "sourceFile": "binance/top.js" + }, + { + "site": "binance", + "name": "trades", + "description": "Recent trades for a trading pair", + "domain": "data-api.binance.vision", + "strategy": "public", + "browser": false, + "args": [ + { + "name": "symbol", + "type": "str", + "required": true, + "positional": true, + "help": "Trading pair symbol (e.g. BTCUSDT, ETHUSDT)" + }, + { + "name": "limit", + "type": "int", + "default": 20, + "required": false, + "help": "Number of trades (max 1000)" + } + ], + "columns": [ + "id", + "price", + "qty", + "quote_qty", + "buyer_maker" + ], + "type": "js", + "modulePath": "binance/trades.js", + "sourceFile": "binance/trades.js" }, { "site": "bloomberg", @@ -1778,7 +2184,8 @@ ], "type": "js", "modulePath": "bloomberg/news.js", - "sourceFile": "bloomberg/news.js" + "sourceFile": "bloomberg/news.js", + "navigateBefore": "https://www.bloomberg.com" }, { "site": "bloomberg", @@ -2733,7 +3140,8 @@ "timeout": 90, "type": "js", "modulePath": "chaoxing/assignments.js", - "sourceFile": "chaoxing/assignments.js" + "sourceFile": "chaoxing/assignments.js", + "navigateBefore": "https://mooc2-ans.chaoxing.com" }, { "site": "chaoxing", @@ -2782,7 +3190,49 @@ "timeout": 90, "type": "js", "modulePath": "chaoxing/exams.js", - "sourceFile": "chaoxing/exams.js" + "sourceFile": "chaoxing/exams.js", + "navigateBefore": "https://mooc2-ans.chaoxing.com" + }, + { + "site": "chatgpt", + "name": "image", + "description": "Generate images with ChatGPT web and save them locally", + "domain": "chatgpt.com", + "strategy": "cookie", + "browser": true, + "args": [ + { + "name": "prompt", + "type": "str", + "required": true, + "positional": true, + "help": "Image prompt to send to ChatGPT" + }, + { + "name": "op", + "type": "str", + "default": "/Users/hao/Pictures/chatgpt", + "required": false, + "help": "Output directory" + }, + { + "name": "sd", + "type": "boolean", + "default": false, + "required": false, + "help": "Skip download shorthand; only show ChatGPT link" + } + ], + "columns": [ + "status", + "file", + "link" + ], + "timeout": 240, + "type": "js", + "modulePath": "chatgpt/image.js", + "sourceFile": "chatgpt/image.js", + "navigateBefore": false }, { "site": "chatgpt-app", @@ -2970,7 +3420,8 @@ ], "type": "js", "modulePath": "chatwise/ask.js", - "sourceFile": "chatwise/ask.js" + "sourceFile": "chatwise/ask.js", + "navigateBefore": true }, { "site": "chatwise", @@ -2994,7 +3445,8 @@ ], "type": "js", "modulePath": "chatwise/export.js", - "sourceFile": "chatwise/export.js" + "sourceFile": "chatwise/export.js", + "navigateBefore": true }, { "site": "chatwise", @@ -3010,7 +3462,8 @@ ], "type": "js", "modulePath": "chatwise/history.js", - "sourceFile": "chatwise/history.js" + "sourceFile": "chatwise/history.js", + "navigateBefore": true }, { "site": "chatwise", @@ -3034,7 +3487,8 @@ ], "type": "js", "modulePath": "chatwise/model.js", - "sourceFile": "chatwise/model.js" + "sourceFile": "chatwise/model.js", + "navigateBefore": true }, { "site": "chatwise", @@ -3049,7 +3503,8 @@ ], "type": "js", "modulePath": "chatwise/read.js", - "sourceFile": "chatwise/read.js" + "sourceFile": "chatwise/read.js", + "navigateBefore": true }, { "site": "chatwise", @@ -3073,7 +3528,8 @@ ], "type": "js", "modulePath": "chatwise/send.js", - "sourceFile": "chatwise/send.js" + "sourceFile": "chatwise/send.js", + "navigateBefore": true }, { "site": "cnki", @@ -3140,7 +3596,8 @@ ], "type": "js", "modulePath": "codex/ask.js", - "sourceFile": "codex/ask.js" + "sourceFile": "codex/ask.js", + "navigateBefore": true }, { "site": "codex", @@ -3164,7 +3621,8 @@ ], "type": "js", "modulePath": "codex/export.js", - "sourceFile": "codex/export.js" + "sourceFile": "codex/export.js", + "navigateBefore": true }, { "site": "codex", @@ -3180,7 +3638,8 @@ ], "type": "js", "modulePath": "codex/extract-diff.js", - "sourceFile": "codex/extract-diff.js" + "sourceFile": "codex/extract-diff.js", + "navigateBefore": true }, { "site": "codex", @@ -3196,7 +3655,8 @@ ], "type": "js", "modulePath": "codex/history.js", - "sourceFile": "codex/history.js" + "sourceFile": "codex/history.js", + "navigateBefore": true }, { "site": "codex", @@ -3220,7 +3680,8 @@ ], "type": "js", "modulePath": "codex/model.js", - "sourceFile": "codex/model.js" + "sourceFile": "codex/model.js", + "navigateBefore": true }, { "site": "codex", @@ -3235,7 +3696,8 @@ ], "type": "js", "modulePath": "codex/read.js", - "sourceFile": "codex/read.js" + "sourceFile": "codex/read.js", + "navigateBefore": true }, { "site": "codex", @@ -3259,7 +3721,8 @@ ], "type": "js", "modulePath": "codex/send.js", - "sourceFile": "codex/send.js" + "sourceFile": "codex/send.js", + "navigateBefore": true }, { "site": "coupang", @@ -3291,7 +3754,8 @@ ], "type": "js", "modulePath": "coupang/add-to-cart.js", - "sourceFile": "coupang/add-to-cart.js" + "sourceFile": "coupang/add-to-cart.js", + "navigateBefore": "https://www.coupang.com" }, { "site": "coupang", @@ -3343,7 +3807,8 @@ ], "type": "js", "modulePath": "coupang/search.js", - "sourceFile": "coupang/search.js" + "sourceFile": "coupang/search.js", + "navigateBefore": "https://www.coupang.com" }, { "site": "ctrip", @@ -3408,7 +3873,8 @@ ], "type": "js", "modulePath": "cursor/ask.js", - "sourceFile": "cursor/ask.js" + "sourceFile": "cursor/ask.js", + "navigateBefore": true }, { "site": "cursor", @@ -3432,7 +3898,8 @@ ], "type": "js", "modulePath": "cursor/composer.js", - "sourceFile": "cursor/composer.js" + "sourceFile": "cursor/composer.js", + "navigateBefore": true }, { "site": "cursor", @@ -3456,7 +3923,8 @@ ], "type": "js", "modulePath": "cursor/export.js", - "sourceFile": "cursor/export.js" + "sourceFile": "cursor/export.js", + "navigateBefore": true }, { "site": "cursor", @@ -3471,7 +3939,8 @@ ], "type": "js", "modulePath": "cursor/extract-code.js", - "sourceFile": "cursor/extract-code.js" + "sourceFile": "cursor/extract-code.js", + "navigateBefore": true }, { "site": "cursor", @@ -3487,7 +3956,8 @@ ], "type": "js", "modulePath": "cursor/history.js", - "sourceFile": "cursor/history.js" + "sourceFile": "cursor/history.js", + "navigateBefore": true }, { "site": "cursor", @@ -3511,7 +3981,8 @@ ], "type": "js", "modulePath": "cursor/model.js", - "sourceFile": "cursor/model.js" + "sourceFile": "cursor/model.js", + "navigateBefore": true }, { "site": "cursor", @@ -3527,7 +3998,8 @@ ], "type": "js", "modulePath": "cursor/read.js", - "sourceFile": "cursor/read.js" + "sourceFile": "cursor/read.js", + "navigateBefore": true }, { "site": "cursor", @@ -3551,7 +4023,8 @@ ], "type": "js", "modulePath": "cursor/send.js", - "sourceFile": "cursor/send.js" + "sourceFile": "cursor/send.js", + "navigateBefore": true }, { "site": "devto", @@ -3739,7 +4212,33 @@ ], "type": "js", "modulePath": "discord-app/channels.js", - "sourceFile": "discord-app/channels.js" + "sourceFile": "discord-app/channels.js", + "navigateBefore": true + }, + { + "site": "discord-app", + "name": "delete", + "description": "Delete a message by its ID in the active Discord channel", + "domain": "localhost", + "strategy": "ui", + "browser": true, + "args": [ + { + "name": "message_id", + "type": "string", + "required": true, + "positional": true, + "help": "The ID of the message to delete (visible via Developer Mode or the read command)" + } + ], + "columns": [ + "status", + "message" + ], + "type": "js", + "modulePath": "discord-app/delete.js", + "sourceFile": "discord-app/delete.js", + "navigateBefore": true }, { "site": "discord-app", @@ -3756,7 +4255,8 @@ ], "type": "js", "modulePath": "discord-app/members.js", - "sourceFile": "discord-app/members.js" + "sourceFile": "discord-app/members.js", + "navigateBefore": true }, { "site": "discord-app", @@ -3781,7 +4281,8 @@ ], "type": "js", "modulePath": "discord-app/read.js", - "sourceFile": "discord-app/read.js" + "sourceFile": "discord-app/read.js", + "navigateBefore": true }, { "site": "discord-app", @@ -3806,7 +4307,8 @@ ], "type": "js", "modulePath": "discord-app/search.js", - "sourceFile": "discord-app/search.js" + "sourceFile": "discord-app/search.js", + "navigateBefore": true }, { "site": "discord-app", @@ -3829,7 +4331,8 @@ ], "type": "js", "modulePath": "discord-app/send.js", - "sourceFile": "discord-app/send.js" + "sourceFile": "discord-app/send.js", + "navigateBefore": true }, { "site": "discord-app", @@ -3845,7 +4348,8 @@ ], "type": "js", "modulePath": "discord-app/servers.js", - "sourceFile": "discord-app/servers.js" + "sourceFile": "discord-app/servers.js", + "navigateBefore": true }, { "site": "discord-app", @@ -3862,7 +4366,8 @@ ], "type": "js", "modulePath": "discord-app/status.js", - "sourceFile": "discord-app/status.js" + "sourceFile": "discord-app/status.js", + "navigateBefore": true }, { "site": "douban", @@ -3892,7 +4397,8 @@ ], "type": "js", "modulePath": "douban/book-hot.js", - "sourceFile": "douban/book-hot.js" + "sourceFile": "douban/book-hot.js", + "navigateBefore": "https://book.douban.com" }, { "site": "douban", @@ -3945,7 +4451,8 @@ ], "type": "js", "modulePath": "douban/download.js", - "sourceFile": "douban/download.js" + "sourceFile": "douban/download.js", + "navigateBefore": "https://movie.douban.com" }, { "site": "douban", @@ -3993,7 +4500,8 @@ ], "type": "js", "modulePath": "douban/marks.js", - "sourceFile": "douban/marks.js" + "sourceFile": "douban/marks.js", + "navigateBefore": "https://movie.douban.com" }, { "site": "douban", @@ -4023,7 +4531,8 @@ ], "type": "js", "modulePath": "douban/movie-hot.js", - "sourceFile": "douban/movie-hot.js" + "sourceFile": "douban/movie-hot.js", + "navigateBefore": "https://movie.douban.com" }, { "site": "douban", @@ -4063,7 +4572,8 @@ ], "type": "js", "modulePath": "douban/photos.js", - "sourceFile": "douban/photos.js" + "sourceFile": "douban/photos.js", + "navigateBefore": "https://movie.douban.com" }, { "site": "douban", @@ -4104,7 +4614,8 @@ ], "type": "js", "modulePath": "douban/reviews.js", - "sourceFile": "douban/reviews.js" + "sourceFile": "douban/reviews.js", + "navigateBefore": "https://movie.douban.com" }, { "site": "douban", @@ -4238,7 +4749,8 @@ ], "type": "js", "modulePath": "douban/top250.js", - "sourceFile": "douban/top250.js" + "sourceFile": "douban/top250.js", + "navigateBefore": "https://movie.douban.com" }, { "site": "doubao", @@ -4497,7 +5009,8 @@ ], "type": "js", "modulePath": "doubao-app/ask.js", - "sourceFile": "doubao-app/ask.js" + "sourceFile": "doubao-app/ask.js", + "navigateBefore": true }, { "site": "doubao-app", @@ -4513,7 +5026,8 @@ ], "type": "js", "modulePath": "doubao-app/dump.js", - "sourceFile": "doubao-app/dump.js" + "sourceFile": "doubao-app/dump.js", + "navigateBefore": true }, { "site": "doubao-app", @@ -4528,7 +5042,8 @@ ], "type": "js", "modulePath": "doubao-app/new.js", - "sourceFile": "doubao-app/new.js" + "sourceFile": "doubao-app/new.js", + "navigateBefore": true }, { "site": "doubao-app", @@ -4544,7 +5059,8 @@ ], "type": "js", "modulePath": "doubao-app/read.js", - "sourceFile": "doubao-app/read.js" + "sourceFile": "doubao-app/read.js", + "navigateBefore": true }, { "site": "doubao-app", @@ -4567,7 +5083,8 @@ ], "type": "js", "modulePath": "doubao-app/screenshot.js", - "sourceFile": "doubao-app/screenshot.js" + "sourceFile": "doubao-app/screenshot.js", + "navigateBefore": true }, { "site": "doubao-app", @@ -4591,7 +5108,8 @@ ], "type": "js", "modulePath": "doubao-app/send.js", - "sourceFile": "doubao-app/send.js" + "sourceFile": "doubao-app/send.js", + "navigateBefore": true }, { "site": "doubao-app", @@ -4608,7 +5126,8 @@ ], "type": "js", "modulePath": "doubao-app/status.js", - "sourceFile": "doubao-app/status.js" + "sourceFile": "doubao-app/status.js", + "navigateBefore": true }, { "site": "douyin", @@ -4625,7 +5144,8 @@ ], "type": "js", "modulePath": "douyin/activities.js", - "sourceFile": "douyin/activities.js" + "sourceFile": "douyin/activities.js", + "navigateBefore": "https://creator.douyin.com" }, { "site": "douyin", @@ -4650,7 +5170,8 @@ ], "type": "js", "modulePath": "douyin/collections.js", - "sourceFile": "douyin/collections.js" + "sourceFile": "douyin/collections.js", + "navigateBefore": "https://creator.douyin.com" }, { "site": "douyin", @@ -4673,7 +5194,8 @@ ], "type": "js", "modulePath": "douyin/delete.js", - "sourceFile": "douyin/delete.js" + "sourceFile": "douyin/delete.js", + "navigateBefore": "https://creator.douyin.com" }, { "site": "douyin", @@ -4755,7 +5277,8 @@ ], "type": "js", "modulePath": "douyin/drafts.js", - "sourceFile": "douyin/drafts.js" + "sourceFile": "douyin/drafts.js", + "navigateBefore": "https://creator.douyin.com" }, { "site": "douyin", @@ -4806,7 +5329,8 @@ ], "type": "js", "modulePath": "douyin/hashtag.js", - "sourceFile": "douyin/hashtag.js" + "sourceFile": "douyin/hashtag.js", + "navigateBefore": "https://creator.douyin.com" }, { "site": "douyin", @@ -4839,7 +5363,8 @@ ], "type": "js", "modulePath": "douyin/location.js", - "sourceFile": "douyin/location.js" + "sourceFile": "douyin/location.js", + "navigateBefore": "https://creator.douyin.com" }, { "site": "douyin", @@ -4858,7 +5383,8 @@ ], "type": "js", "modulePath": "douyin/profile.js", - "sourceFile": "douyin/profile.js" + "sourceFile": "douyin/profile.js", + "navigateBefore": "https://creator.douyin.com" }, { "site": "douyin", @@ -4978,7 +5504,8 @@ ], "type": "js", "modulePath": "douyin/publish.js", - "sourceFile": "douyin/publish.js" + "sourceFile": "douyin/publish.js", + "navigateBefore": "https://creator.douyin.com" }, { "site": "douyin", @@ -5002,7 +5529,8 @@ ], "type": "js", "modulePath": "douyin/stats.js", - "sourceFile": "douyin/stats.js" + "sourceFile": "douyin/stats.js", + "navigateBefore": "https://creator.douyin.com" }, { "site": "douyin", @@ -5039,7 +5567,8 @@ ], "type": "js", "modulePath": "douyin/update.js", - "sourceFile": "douyin/update.js" + "sourceFile": "douyin/update.js", + "navigateBefore": "https://creator.douyin.com" }, { "site": "douyin", @@ -5089,7 +5618,8 @@ ], "type": "js", "modulePath": "douyin/user-videos.js", - "sourceFile": "douyin/user-videos.js" + "sourceFile": "douyin/user-videos.js", + "navigateBefore": "https://www.douyin.com" }, { "site": "douyin", @@ -5137,7 +5667,8 @@ ], "type": "js", "modulePath": "douyin/videos.js", - "sourceFile": "douyin/videos.js" + "sourceFile": "douyin/videos.js", + "navigateBefore": "https://creator.douyin.com" }, { "site": "facebook", @@ -5161,7 +5692,8 @@ ], "type": "js", "modulePath": "facebook/add-friend.js", - "sourceFile": "facebook/add-friend.js" + "sourceFile": "facebook/add-friend.js", + "navigateBefore": "https://www.facebook.com" }, { "site": "facebook", @@ -5185,7 +5717,8 @@ ], "type": "js", "modulePath": "facebook/events.js", - "sourceFile": "facebook/events.js" + "sourceFile": "facebook/events.js", + "navigateBefore": "https://www.facebook.com" }, { "site": "facebook", @@ -5213,7 +5746,8 @@ ], "type": "js", "modulePath": "facebook/feed.js", - "sourceFile": "facebook/feed.js" + "sourceFile": "facebook/feed.js", + "navigateBefore": "https://www.facebook.com" }, { "site": "facebook", @@ -5238,7 +5772,8 @@ ], "type": "js", "modulePath": "facebook/friends.js", - "sourceFile": "facebook/friends.js" + "sourceFile": "facebook/friends.js", + "navigateBefore": "https://www.facebook.com" }, { "site": "facebook", @@ -5264,7 +5799,8 @@ ], "type": "js", "modulePath": "facebook/groups.js", - "sourceFile": "facebook/groups.js" + "sourceFile": "facebook/groups.js", + "navigateBefore": "https://www.facebook.com" }, { "site": "facebook", @@ -5288,7 +5824,8 @@ ], "type": "js", "modulePath": "facebook/join-group.js", - "sourceFile": "facebook/join-group.js" + "sourceFile": "facebook/join-group.js", + "navigateBefore": "https://www.facebook.com" }, { "site": "facebook", @@ -5314,7 +5851,8 @@ ], "type": "js", "modulePath": "facebook/memories.js", - "sourceFile": "facebook/memories.js" + "sourceFile": "facebook/memories.js", + "navigateBefore": "https://www.facebook.com" }, { "site": "facebook", @@ -5339,7 +5877,8 @@ ], "type": "js", "modulePath": "facebook/notifications.js", - "sourceFile": "facebook/notifications.js" + "sourceFile": "facebook/notifications.js", + "navigateBefore": "https://www.facebook.com" }, { "site": "facebook", @@ -5366,7 +5905,8 @@ ], "type": "js", "modulePath": "facebook/profile.js", - "sourceFile": "facebook/profile.js" + "sourceFile": "facebook/profile.js", + "navigateBefore": "https://www.facebook.com" }, { "site": "facebook", @@ -5399,7 +5939,8 @@ ], "type": "js", "modulePath": "facebook/search.js", - "sourceFile": "facebook/search.js" + "sourceFile": "facebook/search.js", + "navigateBefore": "https://www.facebook.com" }, { "site": "gemini", @@ -5559,7 +6100,7 @@ { "name": "op", "type": "str", - "default": "/Users/jakevin/tmp/gemini-images", + "default": "/Users/hao/tmp/gemini-images", "required": false, "help": "Output directory shorthand" }, @@ -5872,7 +6413,8 @@ ], "type": "js", "modulePath": "grok/ask.js", - "sourceFile": "grok/ask.js" + "sourceFile": "grok/ask.js", + "navigateBefore": "https://grok.com" }, { "site": "hackernews", @@ -6211,7 +6753,8 @@ ], "type": "js", "modulePath": "hupu/hot.js", - "sourceFile": "hupu/hot.js" + "sourceFile": "hupu/hot.js", + "navigateBefore": "https://bbs.hupu.com" }, { "site": "hupu", @@ -6652,7 +7195,8 @@ ], "type": "js", "modulePath": "instagram/comment.js", - "sourceFile": "instagram/comment.js" + "sourceFile": "instagram/comment.js", + "navigateBefore": "https://www.instagram.com" }, { "site": "instagram", @@ -6672,7 +7216,7 @@ { "name": "path", "type": "str", - "default": "/Users/jakevin/Downloads/Instagram", + "default": "/Users/hao/Downloads/Instagram", "required": false, "help": "Download directory" } @@ -6708,7 +7252,8 @@ ], "type": "js", "modulePath": "instagram/explore.js", - "sourceFile": "instagram/explore.js" + "sourceFile": "instagram/explore.js", + "navigateBefore": "https://www.instagram.com" }, { "site": "instagram", @@ -6732,7 +7277,8 @@ ], "type": "js", "modulePath": "instagram/follow.js", - "sourceFile": "instagram/follow.js" + "sourceFile": "instagram/follow.js", + "navigateBefore": "https://www.instagram.com" }, { "site": "instagram", @@ -6766,7 +7312,8 @@ ], "type": "js", "modulePath": "instagram/followers.js", - "sourceFile": "instagram/followers.js" + "sourceFile": "instagram/followers.js", + "navigateBefore": "https://www.instagram.com" }, { "site": "instagram", @@ -6800,7 +7347,8 @@ ], "type": "js", "modulePath": "instagram/following.js", - "sourceFile": "instagram/following.js" + "sourceFile": "instagram/following.js", + "navigateBefore": "https://www.instagram.com" }, { "site": "instagram", @@ -6832,7 +7380,8 @@ ], "type": "js", "modulePath": "instagram/like.js", - "sourceFile": "instagram/like.js" + "sourceFile": "instagram/like.js", + "navigateBefore": "https://www.instagram.com" }, { "site": "instagram", @@ -6858,7 +7407,8 @@ "timeout": 120, "type": "js", "modulePath": "instagram/note.js", - "sourceFile": "instagram/note.js" + "sourceFile": "instagram/note.js", + "navigateBefore": true }, { "site": "instagram", @@ -6891,7 +7441,8 @@ "timeout": 300, "type": "js", "modulePath": "instagram/post.js", - "sourceFile": "instagram/post.js" + "sourceFile": "instagram/post.js", + "navigateBefore": true }, { "site": "instagram", @@ -6920,7 +7471,8 @@ ], "type": "js", "modulePath": "instagram/profile.js", - "sourceFile": "instagram/profile.js" + "sourceFile": "instagram/profile.js", + "navigateBefore": "https://www.instagram.com" }, { "site": "instagram", @@ -6953,7 +7505,8 @@ "timeout": 600, "type": "js", "modulePath": "instagram/reel.js", - "sourceFile": "instagram/reel.js" + "sourceFile": "instagram/reel.js", + "navigateBefore": true }, { "site": "instagram", @@ -6985,7 +7538,8 @@ ], "type": "js", "modulePath": "instagram/save.js", - "sourceFile": "instagram/save.js" + "sourceFile": "instagram/save.js", + "navigateBefore": "https://www.instagram.com" }, { "site": "instagram", @@ -7013,7 +7567,8 @@ ], "type": "js", "modulePath": "instagram/saved.js", - "sourceFile": "instagram/saved.js" + "sourceFile": "instagram/saved.js", + "navigateBefore": "https://www.instagram.com" }, { "site": "instagram", @@ -7048,7 +7603,8 @@ ], "type": "js", "modulePath": "instagram/search.js", - "sourceFile": "instagram/search.js" + "sourceFile": "instagram/search.js", + "navigateBefore": "https://www.instagram.com" }, { "site": "instagram", @@ -7074,7 +7630,8 @@ "timeout": 300, "type": "js", "modulePath": "instagram/story.js", - "sourceFile": "instagram/story.js" + "sourceFile": "instagram/story.js", + "navigateBefore": true }, { "site": "instagram", @@ -7098,7 +7655,8 @@ ], "type": "js", "modulePath": "instagram/unfollow.js", - "sourceFile": "instagram/unfollow.js" + "sourceFile": "instagram/unfollow.js", + "navigateBefore": "https://www.instagram.com" }, { "site": "instagram", @@ -7130,7 +7688,8 @@ ], "type": "js", "modulePath": "instagram/unlike.js", - "sourceFile": "instagram/unlike.js" + "sourceFile": "instagram/unlike.js", + "navigateBefore": "https://www.instagram.com" }, { "site": "instagram", @@ -7162,7 +7721,8 @@ ], "type": "js", "modulePath": "instagram/unsave.js", - "sourceFile": "instagram/unsave.js" + "sourceFile": "instagram/unsave.js", + "navigateBefore": "https://www.instagram.com" }, { "site": "instagram", @@ -7197,7 +7757,8 @@ ], "type": "js", "modulePath": "instagram/user.js", - "sourceFile": "instagram/user.js" + "sourceFile": "instagram/user.js", + "navigateBefore": "https://www.instagram.com" }, { "site": "jd", @@ -7317,7 +7878,8 @@ ], "type": "js", "modulePath": "jd/item.js", - "sourceFile": "jd/item.js" + "sourceFile": "jd/item.js", + "navigateBefore": "https://item.jd.com" }, { "site": "jd", @@ -7422,7 +7984,8 @@ ], "type": "js", "modulePath": "jianyu/detail.js", - "sourceFile": "jianyu/detail.js" + "sourceFile": "jianyu/detail.js", + "navigateBefore": "https://www.jianyu360.cn" }, { "site": "jianyu", @@ -7458,7 +8021,8 @@ ], "type": "js", "modulePath": "jianyu/search.js", - "sourceFile": "jianyu/search.js" + "sourceFile": "jianyu/search.js", + "navigateBefore": "https://www.jianyu360.cn" }, { "site": "jike", @@ -7489,7 +8053,8 @@ ], "type": "js", "modulePath": "jike/comment.js", - "sourceFile": "jike/comment.js" + "sourceFile": "jike/comment.js", + "navigateBefore": true }, { "site": "jike", @@ -7513,7 +8078,8 @@ ], "type": "js", "modulePath": "jike/create.js", - "sourceFile": "jike/create.js" + "sourceFile": "jike/create.js", + "navigateBefore": true }, { "site": "jike", @@ -7541,7 +8107,8 @@ ], "type": "js", "modulePath": "jike/feed.js", - "sourceFile": "jike/feed.js" + "sourceFile": "jike/feed.js", + "navigateBefore": "https://web.okjike.com" }, { "site": "jike", @@ -7565,7 +8132,8 @@ ], "type": "js", "modulePath": "jike/like.js", - "sourceFile": "jike/like.js" + "sourceFile": "jike/like.js", + "navigateBefore": true }, { "site": "jike", @@ -7591,7 +8159,8 @@ ], "type": "js", "modulePath": "jike/notifications.js", - "sourceFile": "jike/notifications.js" + "sourceFile": "jike/notifications.js", + "navigateBefore": "https://web.okjike.com" }, { "site": "jike", @@ -7618,7 +8187,8 @@ ], "type": "js", "modulePath": "jike/post.js", - "sourceFile": "jike/post.js" + "sourceFile": "jike/post.js", + "navigateBefore": "https://m.okjike.com" }, { "site": "jike", @@ -7649,7 +8219,8 @@ ], "type": "js", "modulePath": "jike/repost.js", - "sourceFile": "jike/repost.js" + "sourceFile": "jike/repost.js", + "navigateBefore": true }, { "site": "jike", @@ -7684,7 +8255,8 @@ ], "type": "js", "modulePath": "jike/search.js", - "sourceFile": "jike/search.js" + "sourceFile": "jike/search.js", + "navigateBefore": "https://web.okjike.com" }, { "site": "jike", @@ -7719,7 +8291,8 @@ ], "type": "js", "modulePath": "jike/topic.js", - "sourceFile": "jike/topic.js" + "sourceFile": "jike/topic.js", + "navigateBefore": "https://m.okjike.com" }, { "site": "jike", @@ -7754,7 +8327,8 @@ ], "type": "js", "modulePath": "jike/user.js", - "sourceFile": "jike/user.js" + "sourceFile": "jike/user.js", + "navigateBefore": "https://m.okjike.com" }, { "site": "jimeng", @@ -7794,7 +8368,8 @@ ], "type": "js", "modulePath": "jimeng/generate.js", - "sourceFile": "jimeng/generate.js" + "sourceFile": "jimeng/generate.js", + "navigateBefore": "https://jimeng.jianying.com" }, { "site": "jimeng", @@ -7821,7 +8396,8 @@ ], "type": "js", "modulePath": "jimeng/history.js", - "sourceFile": "jimeng/history.js" + "sourceFile": "jimeng/history.js", + "navigateBefore": "https://jimeng.jianying.com" }, { "site": "jimeng", @@ -7837,7 +8413,8 @@ ], "type": "js", "modulePath": "jimeng/new.js", - "sourceFile": "jimeng/new.js" + "sourceFile": "jimeng/new.js", + "navigateBefore": "https://jimeng.jianying.com" }, { "site": "jimeng", @@ -7855,7 +8432,8 @@ ], "type": "js", "modulePath": "jimeng/workspaces.js", - "sourceFile": "jimeng/workspaces.js" + "sourceFile": "jimeng/workspaces.js", + "navigateBefore": "https://jimeng.jianying.com" }, { "site": "ke", @@ -7897,7 +8475,8 @@ ], "type": "js", "modulePath": "ke/chengjiao.js", - "sourceFile": "ke/chengjiao.js" + "sourceFile": "ke/chengjiao.js", + "navigateBefore": "https://ke.com" }, { "site": "ke", @@ -7958,7 +8537,8 @@ ], "type": "js", "modulePath": "ke/ershoufang.js", - "sourceFile": "ke/ershoufang.js" + "sourceFile": "ke/ershoufang.js", + "navigateBefore": "https://ke.com" }, { "site": "ke", @@ -7998,7 +8578,8 @@ ], "type": "js", "modulePath": "ke/xiaoqu.js", - "sourceFile": "ke/xiaoqu.js" + "sourceFile": "ke/xiaoqu.js", + "navigateBefore": "https://ke.com" }, { "site": "ke", @@ -8051,7 +8632,8 @@ ], "type": "js", "modulePath": "ke/zufang.js", - "sourceFile": "ke/zufang.js" + "sourceFile": "ke/zufang.js", + "navigateBefore": "https://ke.com" }, { "site": "lesswrong", @@ -8567,7 +9149,8 @@ ], "type": "js", "modulePath": "linkedin/search.js", - "sourceFile": "linkedin/search.js" + "sourceFile": "linkedin/search.js", + "navigateBefore": "https://www.linkedin.com" }, { "site": "linkedin", @@ -8598,7 +9181,8 @@ ], "type": "js", "modulePath": "linkedin/timeline.js", - "sourceFile": "linkedin/timeline.js" + "sourceFile": "linkedin/timeline.js", + "navigateBefore": "https://www.linkedin.com" }, { "site": "linux-do", @@ -8632,7 +9216,8 @@ ], "type": "js", "modulePath": "linux-do/categories.js", - "sourceFile": "linux-do/categories.js" + "sourceFile": "linux-do/categories.js", + "navigateBefore": "https://linux.do" }, { "site": "linux-do", @@ -8676,7 +9261,8 @@ "replacedBy": "opencli linux-do feed --category ", "type": "js", "modulePath": "linux-do/category.js", - "sourceFile": "linux-do/category.js" + "sourceFile": "linux-do/category.js", + "navigateBefore": "https://linux.do" }, { "site": "linux-do", @@ -8767,7 +9353,8 @@ ], "type": "js", "modulePath": "linux-do/categories.js", - "sourceFile": "linux-do/categories.js" + "sourceFile": "linux-do/categories.js", + "navigateBefore": "https://linux.do" }, { "site": "linux-do", @@ -8812,7 +9399,8 @@ "replacedBy": "opencli linux-do feed --view top --period ", "type": "js", "modulePath": "linux-do/hot.js", - "sourceFile": "linux-do/hot.js" + "sourceFile": "linux-do/hot.js", + "navigateBefore": "https://linux.do" }, { "site": "linux-do", @@ -8842,7 +9430,8 @@ "replacedBy": "opencli linux-do feed --view latest", "type": "js", "modulePath": "linux-do/latest.js", - "sourceFile": "linux-do/latest.js" + "sourceFile": "linux-do/latest.js", + "navigateBefore": "https://linux.do" }, { "site": "linux-do", @@ -8877,7 +9466,8 @@ ], "type": "js", "modulePath": "linux-do/search.js", - "sourceFile": "linux-do/search.js" + "sourceFile": "linux-do/search.js", + "navigateBefore": "https://linux.do" }, { "site": "linux-do", @@ -8903,7 +9493,8 @@ ], "type": "js", "modulePath": "linux-do/tags.js", - "sourceFile": "linux-do/tags.js" + "sourceFile": "linux-do/tags.js", + "navigateBefore": "https://linux.do" }, { "site": "linux-do", @@ -8936,7 +9527,8 @@ ], "type": "js", "modulePath": "linux-do/topic.js", - "sourceFile": "linux-do/topic.js" + "sourceFile": "linux-do/topic.js", + "navigateBefore": "https://linux.do" }, { "site": "linux-do", @@ -8959,7 +9551,8 @@ ], "type": "js", "modulePath": "linux-do/topic-content.js", - "sourceFile": "linux-do/topic-content.js" + "sourceFile": "linux-do/topic-content.js", + "navigateBefore": "https://linux.do" }, { "site": "linux-do", @@ -8994,7 +9587,8 @@ ], "type": "js", "modulePath": "linux-do/user-posts.js", - "sourceFile": "linux-do/user-posts.js" + "sourceFile": "linux-do/user-posts.js", + "navigateBefore": "https://linux.do" }, { "site": "linux-do", @@ -9030,7 +9624,8 @@ ], "type": "js", "modulePath": "linux-do/user-topics.js", - "sourceFile": "linux-do/user-topics.js" + "sourceFile": "linux-do/user-topics.js", + "navigateBefore": "https://linux.do" }, { "site": "lobsters", @@ -9151,6 +9746,129 @@ "modulePath": "lobsters/tag.js", "sourceFile": "lobsters/tag.js" }, + { + "site": "maimai", + "name": "search-talents", + "description": "Search for candidates on Maimai with multi-dimensional filters", + "domain": "maimai.cn", + "strategy": "cookie", + "browser": true, + "args": [ + { + "name": "query", + "type": "str", + "required": true, + "positional": true, + "help": "Search keyword (e.g., \"Java\", \"产品经理\")" + }, + { + "name": "page", + "type": "int", + "default": 0, + "required": false, + "help": "Page number (0-based)" + }, + { + "name": "size", + "type": "int", + "default": 20, + "required": false, + "help": "Results per page" + }, + { + "name": "positions", + "type": "str", + "required": false, + "help": "Positions (e.g., \"运营\", \"Java 开发工程师\")" + }, + { + "name": "companies", + "type": "str", + "required": false, + "help": "Companies, comma-separated (e.g., \"百度\", \"字节跳动,阿里巴巴\")" + }, + { + "name": "schools", + "type": "str", + "required": false, + "help": "Schools, comma-separated (e.g., \"北京大学\", \"清华大学,复旦大学\")" + }, + { + "name": "provinces", + "type": "str", + "required": false, + "help": "Provinces (e.g., \"北京\", \"上海\")" + }, + { + "name": "cities", + "type": "str", + "required": false, + "help": "Cities (e.g., \"北京市\", \"上海市\")" + }, + { + "name": "worktimes", + "type": "str", + "required": false, + "help": "Work years: 1=1-3y, 2=3-5y, 3=5-10y, 4=10+y" + }, + { + "name": "degrees", + "type": "str", + "required": false, + "help": "Education: 1=大专,2=本科,3=硕士,4=博士,5=MBA" + }, + { + "name": "professions", + "type": "str", + "required": false, + "help": "Industries: 01=互联网,02=金融,03=电子,04=通信" + }, + { + "name": "is_211", + "type": "int", + "required": false, + "help": "211 university: 0=any, 1=211" + }, + { + "name": "is_985", + "type": "int", + "required": false, + "help": "985 university: 0=any, 1=985" + }, + { + "name": "sortby", + "type": "int", + "default": 0, + "required": false, + "help": "Sort: 0=relevance, 1=activity, 2=work_years, 3=education" + }, + { + "name": "is_direct_chat", + "type": "int", + "default": 0, + "required": false, + "help": "Direct chat: 0=any, 1=available" + } + ], + "columns": [ + "name", + "job_title", + "company", + "historical_companies", + "location", + "work_year", + "school", + "degree", + "active_status", + "age", + "tags", + "mutual_friends" + ], + "type": "js", + "modulePath": "maimai/search-talents.js", + "sourceFile": "maimai/search-talents.js", + "navigateBefore": "https://maimai.cn" + }, { "site": "medium", "name": "feed", @@ -9184,7 +9902,8 @@ ], "type": "js", "modulePath": "medium/feed.js", - "sourceFile": "medium/feed.js" + "sourceFile": "medium/feed.js", + "navigateBefore": "https://medium.com" }, { "site": "medium", @@ -9220,7 +9939,8 @@ ], "type": "js", "modulePath": "medium/search.js", - "sourceFile": "medium/search.js" + "sourceFile": "medium/search.js", + "navigateBefore": "https://medium.com" }, { "site": "medium", @@ -9255,7 +9975,8 @@ ], "type": "js", "modulePath": "medium/user.js", - "sourceFile": "medium/user.js" + "sourceFile": "medium/user.js", + "navigateBefore": "https://medium.com" }, { "site": "mubu", @@ -9793,7 +10514,8 @@ ], "type": "js", "modulePath": "notion/export.js", - "sourceFile": "notion/export.js" + "sourceFile": "notion/export.js", + "navigateBefore": true }, { "site": "notion", @@ -9810,7 +10532,8 @@ ], "type": "js", "modulePath": "notion/favorites.js", - "sourceFile": "notion/favorites.js" + "sourceFile": "notion/favorites.js", + "navigateBefore": true }, { "site": "notion", @@ -9833,7 +10556,8 @@ ], "type": "js", "modulePath": "notion/new.js", - "sourceFile": "notion/new.js" + "sourceFile": "notion/new.js", + "navigateBefore": true }, { "site": "notion", @@ -9849,7 +10573,8 @@ ], "type": "js", "modulePath": "notion/read.js", - "sourceFile": "notion/read.js" + "sourceFile": "notion/read.js", + "navigateBefore": true }, { "site": "notion", @@ -9873,7 +10598,8 @@ ], "type": "js", "modulePath": "notion/search.js", - "sourceFile": "notion/search.js" + "sourceFile": "notion/search.js", + "navigateBefore": true }, { "site": "notion", @@ -9889,7 +10615,8 @@ ], "type": "js", "modulePath": "notion/sidebar.js", - "sourceFile": "notion/sidebar.js" + "sourceFile": "notion/sidebar.js", + "navigateBefore": true }, { "site": "notion", @@ -9906,7 +10633,8 @@ ], "type": "js", "modulePath": "notion/status.js", - "sourceFile": "notion/status.js" + "sourceFile": "notion/status.js", + "navigateBefore": true }, { "site": "notion", @@ -9929,7 +10657,8 @@ ], "type": "js", "modulePath": "notion/write.js", - "sourceFile": "notion/write.js" + "sourceFile": "notion/write.js", + "navigateBefore": true }, { "site": "ones", @@ -10396,7 +11125,8 @@ ], "type": "js", "modulePath": "pixiv/detail.js", - "sourceFile": "pixiv/detail.js" + "sourceFile": "pixiv/detail.js", + "navigateBefore": "https://www.pixiv.net" }, { "site": "pixiv", @@ -10429,7 +11159,8 @@ ], "type": "js", "modulePath": "pixiv/download.js", - "sourceFile": "pixiv/download.js" + "sourceFile": "pixiv/download.js", + "navigateBefore": "https://www.pixiv.net" }, { "site": "pixiv", @@ -10465,7 +11196,8 @@ ], "type": "js", "modulePath": "pixiv/illusts.js", - "sourceFile": "pixiv/illusts.js" + "sourceFile": "pixiv/illusts.js", + "navigateBefore": "https://www.pixiv.net" }, { "site": "pixiv", @@ -10518,7 +11250,8 @@ ], "type": "js", "modulePath": "pixiv/ranking.js", - "sourceFile": "pixiv/ranking.js" + "sourceFile": "pixiv/ranking.js", + "navigateBefore": "https://www.pixiv.net" }, { "site": "pixiv", @@ -10587,7 +11320,8 @@ ], "type": "js", "modulePath": "pixiv/search.js", - "sourceFile": "pixiv/search.js" + "sourceFile": "pixiv/search.js", + "navigateBefore": "https://www.pixiv.net" }, { "site": "pixiv", @@ -10617,7 +11351,8 @@ ], "type": "js", "modulePath": "pixiv/user.js", - "sourceFile": "pixiv/user.js" + "sourceFile": "pixiv/user.js", + "navigateBefore": "https://www.pixiv.net" }, { "site": "producthunt", @@ -10651,7 +11386,8 @@ ], "type": "js", "modulePath": "producthunt/browse.js", - "sourceFile": "producthunt/browse.js" + "sourceFile": "producthunt/browse.js", + "navigateBefore": true }, { "site": "producthunt", @@ -10677,7 +11413,8 @@ ], "type": "js", "modulePath": "producthunt/hot.js", - "sourceFile": "producthunt/hot.js" + "sourceFile": "producthunt/hot.js", + "navigateBefore": true }, { "site": "producthunt", @@ -10781,7 +11518,8 @@ ], "type": "js", "modulePath": "quark/ls.js", - "sourceFile": "quark/ls.js" + "sourceFile": "quark/ls.js", + "navigateBefore": "https://pan.quark.cn" }, { "site": "quark", @@ -10813,7 +11551,8 @@ ], "type": "js", "modulePath": "quark/mkdir.js", - "sourceFile": "quark/mkdir.js" + "sourceFile": "quark/mkdir.js", + "navigateBefore": "https://pan.quark.cn" }, { "site": "quark", @@ -10848,7 +11587,8 @@ "timeout": 120, "type": "js", "modulePath": "quark/mv.js", - "sourceFile": "quark/mv.js" + "sourceFile": "quark/mv.js", + "navigateBefore": "https://pan.quark.cn" }, { "site": "quark", @@ -10874,7 +11614,8 @@ ], "type": "js", "modulePath": "quark/rename.js", - "sourceFile": "quark/rename.js" + "sourceFile": "quark/rename.js", + "navigateBefore": "https://pan.quark.cn" }, { "site": "quark", @@ -10894,7 +11635,8 @@ ], "type": "js", "modulePath": "quark/rm.js", - "sourceFile": "quark/rm.js" + "sourceFile": "quark/rm.js", + "navigateBefore": "https://pan.quark.cn" }, { "site": "quark", @@ -10950,7 +11692,8 @@ "timeout": 120, "type": "js", "modulePath": "quark/save.js", - "sourceFile": "quark/save.js" + "sourceFile": "quark/save.js", + "navigateBefore": "https://pan.quark.cn" }, { "site": "quark", @@ -10984,7 +11727,8 @@ ], "type": "js", "modulePath": "quark/share-tree.js", - "sourceFile": "quark/share-tree.js" + "sourceFile": "quark/share-tree.js", + "navigateBefore": "https://pan.quark.cn" }, { "site": "reddit", @@ -11015,7 +11759,8 @@ ], "type": "js", "modulePath": "reddit/comment.js", - "sourceFile": "reddit/comment.js" + "sourceFile": "reddit/comment.js", + "navigateBefore": "https://reddit.com" }, { "site": "reddit", @@ -11043,7 +11788,8 @@ ], "type": "js", "modulePath": "reddit/frontpage.js", - "sourceFile": "reddit/frontpage.js" + "sourceFile": "reddit/frontpage.js", + "navigateBefore": "https://reddit.com" }, { "site": "reddit", @@ -11077,7 +11823,8 @@ ], "type": "js", "modulePath": "reddit/hot.js", - "sourceFile": "reddit/hot.js" + "sourceFile": "reddit/hot.js", + "navigateBefore": "https://www.reddit.com" }, { "site": "reddit", @@ -11105,7 +11852,8 @@ ], "type": "js", "modulePath": "reddit/popular.js", - "sourceFile": "reddit/popular.js" + "sourceFile": "reddit/popular.js", + "navigateBefore": "https://reddit.com" }, { "site": "reddit", @@ -11166,7 +11914,8 @@ ], "type": "js", "modulePath": "reddit/read.js", - "sourceFile": "reddit/read.js" + "sourceFile": "reddit/read.js", + "navigateBefore": "https://reddit.com" }, { "site": "reddit", @@ -11197,7 +11946,8 @@ ], "type": "js", "modulePath": "reddit/save.js", - "sourceFile": "reddit/save.js" + "sourceFile": "reddit/save.js", + "navigateBefore": "https://reddit.com" }, { "site": "reddit", @@ -11224,7 +11974,8 @@ ], "type": "js", "modulePath": "reddit/saved.js", - "sourceFile": "reddit/saved.js" + "sourceFile": "reddit/saved.js", + "navigateBefore": "https://reddit.com" }, { "site": "reddit", @@ -11280,7 +12031,8 @@ ], "type": "js", "modulePath": "reddit/search.js", - "sourceFile": "reddit/search.js" + "sourceFile": "reddit/search.js", + "navigateBefore": "https://reddit.com" }, { "site": "reddit", @@ -11328,7 +12080,8 @@ ], "type": "js", "modulePath": "reddit/subreddit.js", - "sourceFile": "reddit/subreddit.js" + "sourceFile": "reddit/subreddit.js", + "navigateBefore": "https://reddit.com" }, { "site": "reddit", @@ -11359,7 +12112,8 @@ ], "type": "js", "modulePath": "reddit/subscribe.js", - "sourceFile": "reddit/subscribe.js" + "sourceFile": "reddit/subscribe.js", + "navigateBefore": "https://reddit.com" }, { "site": "reddit", @@ -11390,7 +12144,8 @@ ], "type": "js", "modulePath": "reddit/upvote.js", - "sourceFile": "reddit/upvote.js" + "sourceFile": "reddit/upvote.js", + "navigateBefore": "https://reddit.com" }, { "site": "reddit", @@ -11417,7 +12172,8 @@ ], "type": "js", "modulePath": "reddit/upvoted.js", - "sourceFile": "reddit/upvoted.js" + "sourceFile": "reddit/upvoted.js", + "navigateBefore": "https://reddit.com" }, { "site": "reddit", @@ -11441,7 +12197,8 @@ ], "type": "js", "modulePath": "reddit/user.js", - "sourceFile": "reddit/user.js" + "sourceFile": "reddit/user.js", + "navigateBefore": "https://reddit.com" }, { "site": "reddit", @@ -11474,7 +12231,8 @@ ], "type": "js", "modulePath": "reddit/user-comments.js", - "sourceFile": "reddit/user-comments.js" + "sourceFile": "reddit/user-comments.js", + "navigateBefore": "https://reddit.com" }, { "site": "reddit", @@ -11508,7 +12266,8 @@ ], "type": "js", "modulePath": "reddit/user-posts.js", - "sourceFile": "reddit/user-posts.js" + "sourceFile": "reddit/user-posts.js", + "navigateBefore": "https://reddit.com" }, { "site": "reuters", @@ -11542,7 +12301,8 @@ ], "type": "js", "modulePath": "reuters/search.js", - "sourceFile": "reuters/search.js" + "sourceFile": "reuters/search.js", + "navigateBefore": "https://www.reuters.com" }, { "site": "sinablog", @@ -11570,7 +12330,8 @@ ], "type": "js", "modulePath": "sinablog/article.js", - "sourceFile": "sinablog/article.js" + "sourceFile": "sinablog/article.js", + "navigateBefore": "https://blog.sina.com.cn" }, { "site": "sinablog", @@ -11598,7 +12359,8 @@ ], "type": "js", "modulePath": "sinablog/hot.js", - "sourceFile": "sinablog/hot.js" + "sourceFile": "sinablog/hot.js", + "navigateBefore": "https://blog.sina.com.cn" }, { "site": "sinablog", @@ -11668,7 +12430,8 @@ ], "type": "js", "modulePath": "sinablog/user.js", - "sourceFile": "sinablog/user.js" + "sourceFile": "sinablog/user.js", + "navigateBefore": "https://blog.sina.com.cn" }, { "site": "sinafinance", @@ -11719,7 +12482,8 @@ ], "type": "js", "modulePath": "sinafinance/rolling-news.js", - "sourceFile": "sinafinance/rolling-news.js" + "sourceFile": "sinafinance/rolling-news.js", + "navigateBefore": "https://finance.sina.com.cn/roll" }, { "site": "sinafinance", @@ -11830,7 +12594,8 @@ ], "type": "js", "modulePath": "smzdm/search.js", - "sourceFile": "smzdm/search.js" + "sourceFile": "smzdm/search.js", + "navigateBefore": "https://www.smzdm.com" }, { "site": "spotify", @@ -12237,7 +13002,8 @@ ], "type": "js", "modulePath": "substack/feed.js", - "sourceFile": "substack/feed.js" + "sourceFile": "substack/feed.js", + "navigateBefore": "https://substack.com" }, { "site": "substack", @@ -12271,7 +13037,8 @@ ], "type": "js", "modulePath": "substack/publication.js", - "sourceFile": "substack/publication.js" + "sourceFile": "substack/publication.js", + "navigateBefore": "https://substack.com" }, { "site": "substack", @@ -12682,7 +13449,8 @@ ], "type": "js", "modulePath": "tiktok/comment.js", - "sourceFile": "tiktok/comment.js" + "sourceFile": "tiktok/comment.js", + "navigateBefore": "https://www.tiktok.com" }, { "site": "tiktok", @@ -12708,7 +13476,8 @@ ], "type": "js", "modulePath": "tiktok/explore.js", - "sourceFile": "tiktok/explore.js" + "sourceFile": "tiktok/explore.js", + "navigateBefore": "https://www.tiktok.com" }, { "site": "tiktok", @@ -12732,7 +13501,8 @@ ], "type": "js", "modulePath": "tiktok/follow.js", - "sourceFile": "tiktok/follow.js" + "sourceFile": "tiktok/follow.js", + "navigateBefore": "https://www.tiktok.com" }, { "site": "tiktok", @@ -12757,7 +13527,8 @@ ], "type": "js", "modulePath": "tiktok/following.js", - "sourceFile": "tiktok/following.js" + "sourceFile": "tiktok/following.js", + "navigateBefore": "https://www.tiktok.com" }, { "site": "tiktok", @@ -12782,7 +13553,8 @@ ], "type": "js", "modulePath": "tiktok/friends.js", - "sourceFile": "tiktok/friends.js" + "sourceFile": "tiktok/friends.js", + "navigateBefore": "https://www.tiktok.com" }, { "site": "tiktok", @@ -12807,7 +13579,8 @@ ], "type": "js", "modulePath": "tiktok/like.js", - "sourceFile": "tiktok/like.js" + "sourceFile": "tiktok/like.js", + "navigateBefore": "https://www.tiktok.com" }, { "site": "tiktok", @@ -12833,7 +13606,8 @@ ], "type": "js", "modulePath": "tiktok/live.js", - "sourceFile": "tiktok/live.js" + "sourceFile": "tiktok/live.js", + "navigateBefore": "https://www.tiktok.com" }, { "site": "tiktok", @@ -12871,7 +13645,8 @@ ], "type": "js", "modulePath": "tiktok/notifications.js", - "sourceFile": "tiktok/notifications.js" + "sourceFile": "tiktok/notifications.js", + "navigateBefore": "https://www.tiktok.com" }, { "site": "tiktok", @@ -12901,7 +13676,8 @@ ], "type": "js", "modulePath": "tiktok/profile.js", - "sourceFile": "tiktok/profile.js" + "sourceFile": "tiktok/profile.js", + "navigateBefore": "https://www.tiktok.com" }, { "site": "tiktok", @@ -12925,7 +13701,8 @@ ], "type": "js", "modulePath": "tiktok/save.js", - "sourceFile": "tiktok/save.js" + "sourceFile": "tiktok/save.js", + "navigateBefore": "https://www.tiktok.com" }, { "site": "tiktok", @@ -12962,7 +13739,8 @@ ], "type": "js", "modulePath": "tiktok/search.js", - "sourceFile": "tiktok/search.js" + "sourceFile": "tiktok/search.js", + "navigateBefore": "https://www.tiktok.com" }, { "site": "tiktok", @@ -12986,7 +13764,8 @@ ], "type": "js", "modulePath": "tiktok/unfollow.js", - "sourceFile": "tiktok/unfollow.js" + "sourceFile": "tiktok/unfollow.js", + "navigateBefore": "https://www.tiktok.com" }, { "site": "tiktok", @@ -13011,7 +13790,8 @@ ], "type": "js", "modulePath": "tiktok/unlike.js", - "sourceFile": "tiktok/unlike.js" + "sourceFile": "tiktok/unlike.js", + "navigateBefore": "https://www.tiktok.com" }, { "site": "tiktok", @@ -13035,7 +13815,8 @@ ], "type": "js", "modulePath": "tiktok/unsave.js", - "sourceFile": "tiktok/unsave.js" + "sourceFile": "tiktok/unsave.js", + "navigateBefore": "https://www.tiktok.com" }, { "site": "tiktok", @@ -13067,7 +13848,8 @@ ], "type": "js", "modulePath": "tiktok/user.js", - "sourceFile": "tiktok/user.js" + "sourceFile": "tiktok/user.js", + "navigateBefore": "https://www.tiktok.com" }, { "site": "twitter", @@ -13101,7 +13883,8 @@ "timeout": 600, "type": "js", "modulePath": "twitter/accept.js", - "sourceFile": "twitter/accept.js" + "sourceFile": "twitter/accept.js", + "navigateBefore": true }, { "site": "twitter", @@ -13127,7 +13910,8 @@ ], "type": "js", "modulePath": "twitter/article.js", - "sourceFile": "twitter/article.js" + "sourceFile": "twitter/article.js", + "navigateBefore": "https://x.com" }, { "site": "twitter", @@ -13151,7 +13935,8 @@ ], "type": "js", "modulePath": "twitter/block.js", - "sourceFile": "twitter/block.js" + "sourceFile": "twitter/block.js", + "navigateBefore": true }, { "site": "twitter", @@ -13175,7 +13960,8 @@ ], "type": "js", "modulePath": "twitter/bookmark.js", - "sourceFile": "twitter/bookmark.js" + "sourceFile": "twitter/bookmark.js", + "navigateBefore": true }, { "site": "twitter", @@ -13193,17 +13979,18 @@ "help": "" } ], - "columns": [ - "author", - "text", - "likes", - "retweets", - "bookmarks", - "url" - ], + "columns": [ + "author", + "text", + "likes", + "retweets", + "bookmarks", + "url" + ], "type": "js", "modulePath": "twitter/bookmarks.js", - "sourceFile": "twitter/bookmarks.js" + "sourceFile": "twitter/bookmarks.js", + "navigateBefore": "https://x.com" }, { "site": "twitter", @@ -13227,7 +14014,8 @@ ], "type": "js", "modulePath": "twitter/delete.js", - "sourceFile": "twitter/delete.js" + "sourceFile": "twitter/delete.js", + "navigateBefore": true }, { "site": "twitter", @@ -13273,7 +14061,8 @@ ], "type": "js", "modulePath": "twitter/download.js", - "sourceFile": "twitter/download.js" + "sourceFile": "twitter/download.js", + "navigateBefore": "https://x.com" }, { "site": "twitter", @@ -13297,7 +14086,8 @@ ], "type": "js", "modulePath": "twitter/follow.js", - "sourceFile": "twitter/follow.js" + "sourceFile": "twitter/follow.js", + "navigateBefore": true }, { "site": "twitter", @@ -13330,7 +14120,8 @@ ], "type": "js", "modulePath": "twitter/followers.js", - "sourceFile": "twitter/followers.js" + "sourceFile": "twitter/followers.js", + "navigateBefore": true }, { "site": "twitter", @@ -13363,7 +14154,8 @@ ], "type": "js", "modulePath": "twitter/following.js", - "sourceFile": "twitter/following.js" + "sourceFile": "twitter/following.js", + "navigateBefore": true }, { "site": "twitter", @@ -13387,7 +14179,8 @@ ], "type": "js", "modulePath": "twitter/hide-reply.js", - "sourceFile": "twitter/hide-reply.js" + "sourceFile": "twitter/hide-reply.js", + "navigateBefore": true }, { "site": "twitter", @@ -13411,7 +14204,8 @@ ], "type": "js", "modulePath": "twitter/like.js", - "sourceFile": "twitter/like.js" + "sourceFile": "twitter/like.js", + "navigateBefore": true }, { "site": "twitter", @@ -13445,7 +14239,8 @@ ], "type": "js", "modulePath": "twitter/likes.js", - "sourceFile": "twitter/likes.js" + "sourceFile": "twitter/likes.js", + "navigateBefore": "https://x.com" }, { "site": "twitter", @@ -13478,7 +14273,8 @@ ], "type": "js", "modulePath": "twitter/lists.js", - "sourceFile": "twitter/lists.js" + "sourceFile": "twitter/lists.js", + "navigateBefore": "https://x.com" }, { "site": "twitter", @@ -13505,7 +14301,8 @@ ], "type": "js", "modulePath": "twitter/notifications.js", - "sourceFile": "twitter/notifications.js" + "sourceFile": "twitter/notifications.js", + "navigateBefore": true }, { "site": "twitter", @@ -13536,7 +14333,40 @@ ], "type": "js", "modulePath": "twitter/post.js", - "sourceFile": "twitter/post.js" + "sourceFile": "twitter/post.js", + "navigateBefore": true + }, + { + "site": "twitter", + "name": "post-media", + "description": "Post a tweet with an attached video or image", + "domain": "x.com", + "strategy": "ui", + "browser": true, + "args": [ + { + "name": "text", + "type": "string", + "required": true, + "positional": true, + "help": "Tweet text" + }, + { + "name": "media", + "type": "string", + "required": true, + "help": "Path to a local video or image file" + } + ], + "columns": [ + "status", + "tweet_id", + "message" + ], + "type": "js", + "modulePath": "twitter/post-media.js", + "sourceFile": "twitter/post-media.js", + "navigateBefore": true }, { "site": "twitter", @@ -13569,7 +14399,8 @@ ], "type": "js", "modulePath": "twitter/profile.js", - "sourceFile": "twitter/profile.js" + "sourceFile": "twitter/profile.js", + "navigateBefore": "https://x.com" }, { "site": "twitter", @@ -13613,7 +14444,8 @@ ], "type": "js", "modulePath": "twitter/reply.js", - "sourceFile": "twitter/reply.js" + "sourceFile": "twitter/reply.js", + "navigateBefore": true }, { "site": "twitter", @@ -13654,7 +14486,8 @@ "timeout": 600, "type": "js", "modulePath": "twitter/reply-dm.js", - "sourceFile": "twitter/reply-dm.js" + "sourceFile": "twitter/reply-dm.js", + "navigateBefore": true }, { "site": "twitter", @@ -13701,7 +14534,8 @@ ], "type": "js", "modulePath": "twitter/search.js", - "sourceFile": "twitter/search.js" + "sourceFile": "twitter/search.js", + "navigateBefore": true }, { "site": "twitter", @@ -13736,7 +14570,8 @@ ], "type": "js", "modulePath": "twitter/thread.js", - "sourceFile": "twitter/thread.js" + "sourceFile": "twitter/thread.js", + "navigateBefore": "https://x.com" }, { "site": "twitter", @@ -13778,7 +14613,8 @@ ], "type": "js", "modulePath": "twitter/timeline.js", - "sourceFile": "twitter/timeline.js" + "sourceFile": "twitter/timeline.js", + "navigateBefore": "https://x.com" }, { "site": "twitter", @@ -13804,7 +14640,8 @@ ], "type": "js", "modulePath": "twitter/trending.js", - "sourceFile": "twitter/trending.js" + "sourceFile": "twitter/trending.js", + "navigateBefore": "https://x.com" }, { "site": "twitter", @@ -13828,7 +14665,8 @@ ], "type": "js", "modulePath": "twitter/unblock.js", - "sourceFile": "twitter/unblock.js" + "sourceFile": "twitter/unblock.js", + "navigateBefore": true }, { "site": "twitter", @@ -13852,7 +14690,8 @@ ], "type": "js", "modulePath": "twitter/unbookmark.js", - "sourceFile": "twitter/unbookmark.js" + "sourceFile": "twitter/unbookmark.js", + "navigateBefore": true }, { "site": "twitter", @@ -13876,7 +14715,8 @@ ], "type": "js", "modulePath": "twitter/unfollow.js", - "sourceFile": "twitter/unfollow.js" + "sourceFile": "twitter/unfollow.js", + "navigateBefore": true }, { "site": "uiverse", @@ -13941,8 +14781,8 @@ { "name": "padding", "type": "int", - "required": false, "default": 8, + "required": false, "help": "Extra padding around the captured preview in pixels" } ], @@ -13971,7 +14811,8 @@ ], "type": "js", "modulePath": "v2ex/daily.js", - "sourceFile": "v2ex/daily.js" + "sourceFile": "v2ex/daily.js", + "navigateBefore": "https://www.v2ex.com" }, { "site": "v2ex", @@ -14045,7 +14886,8 @@ ], "type": "js", "modulePath": "v2ex/me.js", - "sourceFile": "v2ex/me.js" + "sourceFile": "v2ex/me.js", + "navigateBefore": "https://www.v2ex.com" }, { "site": "v2ex", @@ -14159,7 +15001,8 @@ ], "type": "js", "modulePath": "v2ex/notifications.js", - "sourceFile": "v2ex/notifications.js" + "sourceFile": "v2ex/notifications.js", + "navigateBefore": "https://www.v2ex.com" }, { "site": "v2ex", @@ -14337,16 +15180,28 @@ ], "type": "js", "modulePath": "weibo/comments.js", - "sourceFile": "weibo/comments.js" + "sourceFile": "weibo/comments.js", + "navigateBefore": "https://weibo.com" }, { "site": "weibo", "name": "feed", - "description": "Weibo home timeline (posts from followed users)", + "description": "Fetch Weibo timeline (for-you or following)", "domain": "weibo.com", "strategy": "cookie", "browser": true, "args": [ + { + "name": "type", + "type": "str", + "default": "for-you", + "required": false, + "help": "Timeline type: for-you (algorithmic) or following (chronological)", + "choices": [ + "for-you", + "following" + ] + }, { "name": "limit", "type": "int", @@ -14366,7 +15221,8 @@ ], "type": "js", "modulePath": "weibo/feed.js", - "sourceFile": "weibo/feed.js" + "sourceFile": "weibo/feed.js", + "navigateBefore": "https://weibo.com" }, { "site": "weibo", @@ -14394,7 +15250,8 @@ ], "type": "js", "modulePath": "weibo/hot.js", - "sourceFile": "weibo/hot.js" + "sourceFile": "weibo/hot.js", + "navigateBefore": "https://weibo.com" }, { "site": "weibo", @@ -14415,7 +15272,8 @@ ], "type": "js", "modulePath": "weibo/me.js", - "sourceFile": "weibo/me.js" + "sourceFile": "weibo/me.js", + "navigateBefore": "https://weibo.com" }, { "site": "weibo", @@ -14439,7 +15297,8 @@ ], "type": "js", "modulePath": "weibo/post.js", - "sourceFile": "weibo/post.js" + "sourceFile": "weibo/post.js", + "navigateBefore": "https://weibo.com" }, { "site": "weibo", @@ -14473,7 +15332,8 @@ ], "type": "js", "modulePath": "weibo/search.js", - "sourceFile": "weibo/search.js" + "sourceFile": "weibo/search.js", + "navigateBefore": "https://weibo.com" }, { "site": "weibo", @@ -14504,7 +15364,8 @@ ], "type": "js", "modulePath": "weibo/user.js", - "sourceFile": "weibo/user.js" + "sourceFile": "weibo/user.js", + "navigateBefore": "https://weibo.com" }, { "site": "weixin", @@ -14544,7 +15405,8 @@ ], "type": "js", "modulePath": "weixin/download.js", - "sourceFile": "weixin/download.js" + "sourceFile": "weixin/download.js", + "navigateBefore": "https://mp.weixin.qq.com" }, { "site": "weread", @@ -14572,7 +15434,8 @@ ], "type": "js", "modulePath": "weread/book.js", - "sourceFile": "weread/book.js" + "sourceFile": "weread/book.js", + "navigateBefore": "https://weread.qq.com" }, { "site": "weread", @@ -14604,7 +15467,8 @@ ], "type": "js", "modulePath": "weread/highlights.js", - "sourceFile": "weread/highlights.js" + "sourceFile": "weread/highlights.js", + "navigateBefore": "https://weread.qq.com" }, { "site": "weread", @@ -14622,7 +15486,8 @@ ], "type": "js", "modulePath": "weread/notebooks.js", - "sourceFile": "weread/notebooks.js" + "sourceFile": "weread/notebooks.js", + "navigateBefore": "https://weread.qq.com" }, { "site": "weread", @@ -14655,7 +15520,8 @@ ], "type": "js", "modulePath": "weread/notes.js", - "sourceFile": "weread/notes.js" + "sourceFile": "weread/notes.js", + "navigateBefore": "https://weread.qq.com" }, { "site": "weread", @@ -14751,7 +15617,8 @@ ], "type": "js", "modulePath": "weread/shelf.js", - "sourceFile": "weread/shelf.js" + "sourceFile": "weread/shelf.js", + "navigateBefore": "https://weread.qq.com" }, { "site": "wikipedia", @@ -15019,7 +15886,8 @@ ], "type": "js", "modulePath": "xiaoe/catalog.js", - "sourceFile": "xiaoe/catalog.js" + "sourceFile": "xiaoe/catalog.js", + "navigateBefore": "https://h5.xet.citv.cn" }, { "site": "xiaoe", @@ -15044,7 +15912,8 @@ ], "type": "js", "modulePath": "xiaoe/content.js", - "sourceFile": "xiaoe/content.js" + "sourceFile": "xiaoe/content.js", + "navigateBefore": "https://h5.xet.citv.cn" }, { "site": "xiaoe", @@ -15061,7 +15930,8 @@ ], "type": "js", "modulePath": "xiaoe/courses.js", - "sourceFile": "xiaoe/courses.js" + "sourceFile": "xiaoe/courses.js", + "navigateBefore": "https://study.xiaoe-tech.com" }, { "site": "xiaoe", @@ -15088,7 +15958,8 @@ ], "type": "js", "modulePath": "xiaoe/detail.js", - "sourceFile": "xiaoe/detail.js" + "sourceFile": "xiaoe/detail.js", + "navigateBefore": "https://h5.xet.citv.cn" }, { "site": "xiaoe", @@ -15115,7 +15986,8 @@ ], "type": "js", "modulePath": "xiaoe/play-url.js", - "sourceFile": "xiaoe/play-url.js" + "sourceFile": "xiaoe/play-url.js", + "navigateBefore": "https://h5.xet.citv.cn" }, { "site": "xiaohongshu", @@ -15130,7 +16002,7 @@ "type": "str", "required": true, "positional": true, - "help": "Note ID or full URL (preserves xsec_token for access)" + "help": "Full Xiaohongshu note URL with xsec_token" }, { "name": "limit", @@ -15317,7 +16189,7 @@ "type": "str", "required": true, "positional": true, - "help": "Note ID, full URL, or short link" + "help": "Full Xiaohongshu note URL with xsec_token, or xhslink short link" }, { "name": "output", @@ -15363,7 +16235,8 @@ ], "type": "js", "modulePath": "xiaohongshu/feed.js", - "sourceFile": "xiaohongshu/feed.js" + "sourceFile": "xiaohongshu/feed.js", + "navigateBefore": true }, { "site": "xiaohongshu", @@ -15378,7 +16251,7 @@ "type": "str", "required": true, "positional": true, - "help": "Note ID or full URL (preserves xsec_token for access)" + "help": "Full Xiaohongshu note URL with xsec_token" } ], "columns": [ @@ -15423,7 +16296,8 @@ ], "type": "js", "modulePath": "xiaohongshu/notifications.js", - "sourceFile": "xiaohongshu/notifications.js" + "sourceFile": "xiaohongshu/notifications.js", + "navigateBefore": true }, { "site": "xiaohongshu", @@ -15711,7 +16585,8 @@ ], "type": "js", "modulePath": "xueqiu/earnings-date.js", - "sourceFile": "xueqiu/earnings-date.js" + "sourceFile": "xueqiu/earnings-date.js", + "navigateBefore": "https://xueqiu.com" }, { "site": "xueqiu", @@ -15745,7 +16620,8 @@ ], "type": "js", "modulePath": "xueqiu/feed.js", - "sourceFile": "xueqiu/feed.js" + "sourceFile": "xueqiu/feed.js", + "navigateBefore": "https://xueqiu.com" }, { "site": "xueqiu", @@ -15814,7 +16690,8 @@ ], "type": "js", "modulePath": "xueqiu/groups.js", - "sourceFile": "xueqiu/groups.js" + "sourceFile": "xueqiu/groups.js", + "navigateBefore": "https://xueqiu.com" }, { "site": "xueqiu", @@ -15841,7 +16718,8 @@ ], "type": "js", "modulePath": "xueqiu/hot.js", - "sourceFile": "xueqiu/hot.js" + "sourceFile": "xueqiu/hot.js", + "navigateBefore": "https://xueqiu.com" }, { "site": "xueqiu", @@ -15876,7 +16754,8 @@ ], "type": "js", "modulePath": "xueqiu/hot-stock.js", - "sourceFile": "xueqiu/hot-stock.js" + "sourceFile": "xueqiu/hot-stock.js", + "navigateBefore": "https://xueqiu.com" }, { "site": "xueqiu", @@ -15911,7 +16790,8 @@ ], "type": "js", "modulePath": "xueqiu/kline.js", - "sourceFile": "xueqiu/kline.js" + "sourceFile": "xueqiu/kline.js", + "navigateBefore": "https://xueqiu.com" }, { "site": "xueqiu", @@ -15946,7 +16826,8 @@ ], "type": "js", "modulePath": "xueqiu/search.js", - "sourceFile": "xueqiu/search.js" + "sourceFile": "xueqiu/search.js", + "navigateBefore": "https://xueqiu.com" }, { "site": "xueqiu", @@ -15973,7 +16854,8 @@ ], "type": "js", "modulePath": "xueqiu/stock.js", - "sourceFile": "xueqiu/stock.js" + "sourceFile": "xueqiu/stock.js", + "navigateBefore": "https://xueqiu.com" }, { "site": "xueqiu", @@ -16006,7 +16888,8 @@ ], "type": "js", "modulePath": "xueqiu/watchlist.js", - "sourceFile": "xueqiu/watchlist.js" + "sourceFile": "xueqiu/watchlist.js", + "navigateBefore": "https://xueqiu.com" }, { "site": "yahoo-finance", @@ -16038,7 +16921,8 @@ ], "type": "js", "modulePath": "yahoo-finance/quote.js", - "sourceFile": "yahoo-finance/quote.js" + "sourceFile": "yahoo-finance/quote.js", + "navigateBefore": "https://finance.yahoo.com" }, { "site": "yollomi", @@ -16085,7 +16969,8 @@ ], "type": "js", "modulePath": "yollomi/background.js", - "sourceFile": "yollomi/background.js" + "sourceFile": "yollomi/background.js", + "navigateBefore": "https://yollomi.com" }, { "site": "yollomi", @@ -16144,7 +17029,8 @@ ], "type": "js", "modulePath": "yollomi/edit.js", - "sourceFile": "yollomi/edit.js" + "sourceFile": "yollomi/edit.js", + "navigateBefore": "https://yollomi.com" }, { "site": "yollomi", @@ -16189,7 +17075,8 @@ ], "type": "js", "modulePath": "yollomi/face-swap.js", - "sourceFile": "yollomi/face-swap.js" + "sourceFile": "yollomi/face-swap.js", + "navigateBefore": "https://yollomi.com" }, { "site": "yollomi", @@ -16257,7 +17144,8 @@ ], "type": "js", "modulePath": "yollomi/generate.js", - "sourceFile": "yollomi/generate.js" + "sourceFile": "yollomi/generate.js", + "navigateBefore": "https://yollomi.com" }, { "site": "yollomi", @@ -16335,7 +17223,8 @@ ], "type": "js", "modulePath": "yollomi/object-remover.js", - "sourceFile": "yollomi/object-remover.js" + "sourceFile": "yollomi/object-remover.js", + "navigateBefore": "https://yollomi.com" }, { "site": "yollomi", @@ -16375,7 +17264,8 @@ ], "type": "js", "modulePath": "yollomi/remove-bg.js", - "sourceFile": "yollomi/remove-bg.js" + "sourceFile": "yollomi/remove-bg.js", + "navigateBefore": "https://yollomi.com" }, { "site": "yollomi", @@ -16415,7 +17305,8 @@ ], "type": "js", "modulePath": "yollomi/restore.js", - "sourceFile": "yollomi/restore.js" + "sourceFile": "yollomi/restore.js", + "navigateBefore": "https://yollomi.com" }, { "site": "yollomi", @@ -16472,7 +17363,8 @@ ], "type": "js", "modulePath": "yollomi/try-on.js", - "sourceFile": "yollomi/try-on.js" + "sourceFile": "yollomi/try-on.js", + "navigateBefore": "https://yollomi.com" }, { "site": "yollomi", @@ -16498,7 +17390,8 @@ ], "type": "js", "modulePath": "yollomi/upload.js", - "sourceFile": "yollomi/upload.js" + "sourceFile": "yollomi/upload.js", + "navigateBefore": "https://yollomi.com" }, { "site": "yollomi", @@ -16550,7 +17443,8 @@ ], "type": "js", "modulePath": "yollomi/upscale.js", - "sourceFile": "yollomi/upscale.js" + "sourceFile": "yollomi/upscale.js", + "navigateBefore": "https://yollomi.com" }, { "site": "yollomi", @@ -16618,7 +17512,8 @@ ], "type": "js", "modulePath": "yollomi/video.js", - "sourceFile": "yollomi/video.js" + "sourceFile": "yollomi/video.js", + "navigateBefore": "https://yollomi.com" }, { "site": "youtube", @@ -16649,7 +17544,8 @@ ], "type": "js", "modulePath": "youtube/channel.js", - "sourceFile": "youtube/channel.js" + "sourceFile": "youtube/channel.js", + "navigateBefore": "https://www.youtube.com" }, { "site": "youtube", @@ -16684,7 +17580,129 @@ ], "type": "js", "modulePath": "youtube/comments.js", - "sourceFile": "youtube/comments.js" + "sourceFile": "youtube/comments.js", + "navigateBefore": "https://www.youtube.com" + }, + { + "site": "youtube", + "name": "feed", + "description": "Get YouTube homepage recommended videos", + "domain": "www.youtube.com", + "strategy": "cookie", + "browser": true, + "args": [ + { + "name": "limit", + "type": "int", + "default": 20, + "required": false, + "help": "Max videos to return (default 20, max 100)" + } + ], + "columns": [ + "rank", + "title", + "channel", + "views", + "duration", + "published", + "url" + ], + "type": "js", + "modulePath": "youtube/feed.js", + "sourceFile": "youtube/feed.js", + "navigateBefore": "https://www.youtube.com" + }, + { + "site": "youtube", + "name": "history", + "description": "Get YouTube watch history", + "domain": "www.youtube.com", + "strategy": "cookie", + "browser": true, + "args": [ + { + "name": "limit", + "type": "int", + "default": 30, + "required": false, + "help": "Max videos to return (default 30, max 200)" + } + ], + "columns": [ + "rank", + "title", + "channel", + "views", + "duration", + "url" + ], + "type": "js", + "modulePath": "youtube/history.js", + "sourceFile": "youtube/history.js", + "navigateBefore": "https://www.youtube.com" + }, + { + "site": "youtube", + "name": "like", + "description": "Like a YouTube video", + "domain": "www.youtube.com", + "strategy": "cookie", + "browser": true, + "args": [ + { + "name": "url", + "type": "str", + "required": true, + "positional": true, + "help": "YouTube video URL or video ID" + } + ], + "columns": [ + "status", + "message" + ], + "type": "js", + "modulePath": "youtube/like.js", + "sourceFile": "youtube/like.js", + "navigateBefore": "https://www.youtube.com" + }, + { + "site": "youtube", + "name": "playlist", + "description": "Get YouTube playlist info and video list", + "domain": "www.youtube.com", + "strategy": "cookie", + "browser": true, + "args": [ + { + "name": "id", + "type": "str", + "required": true, + "positional": true, + "help": "Playlist URL or playlist ID (PLxxxxxx)" + }, + { + "name": "limit", + "type": "int", + "default": 50, + "required": false, + "help": "Max videos to return (default 50, max 200)" + } + ], + "columns": [ + "rank", + "title", + "channel", + "duration", + "views", + "published", + "url" + ], + "type": "js", + "modulePath": "youtube/playlist.js", + "sourceFile": "youtube/playlist.js", + "navigateBefore": "https://www.youtube.com" }, { "site": "youtube", @@ -16741,7 +17759,61 @@ ], "type": "js", "modulePath": "youtube/search.js", - "sourceFile": "youtube/search.js" + "sourceFile": "youtube/search.js", + "navigateBefore": "https://www.youtube.com" + }, + { + "site": "youtube", + "name": "subscribe", + "description": "Subscribe to a YouTube channel", + "domain": "www.youtube.com", + "strategy": "cookie", + "browser": true, + "args": [ + { + "name": "channel", + "type": "str", + "required": true, + "positional": true, + "help": "Channel ID (UCxxxx) or handle (@name)" + } + ], + "columns": [ + "status", + "message" + ], + "type": "js", + "modulePath": "youtube/subscribe.js", + "sourceFile": "youtube/subscribe.js", + "navigateBefore": "https://www.youtube.com" + }, + { + "site": "youtube", + "name": "subscriptions", + "description": "List subscribed YouTube channels", + "domain": "www.youtube.com", + "strategy": "cookie", + "browser": true, + "args": [ + { + "name": "limit", + "type": "int", + "default": 50, + "required": false, + "help": "Max channels to return (default 50)" + } + ], + "columns": [ + "rank", + "name", + "handle", + "subscribers", + "url" + ], + "type": "js", + "modulePath": "youtube/subscriptions.js", + "sourceFile": "youtube/subscriptions.js", + "navigateBefore": "https://www.youtube.com" }, { "site": "youtube", @@ -16774,7 +17846,58 @@ ], "type": "js", "modulePath": "youtube/transcript.js", - "sourceFile": "youtube/transcript.js" + "sourceFile": "youtube/transcript.js", + "navigateBefore": "https://www.youtube.com" + }, + { + "site": "youtube", + "name": "unlike", + "description": "Remove like from a YouTube video", + "domain": "www.youtube.com", + "strategy": "cookie", + "browser": true, + "args": [ + { + "name": "url", + "type": "str", + "required": true, + "positional": true, + "help": "YouTube video URL or video ID" + } + ], + "columns": [ + "status", + "message" + ], + "type": "js", + "modulePath": "youtube/unlike.js", + "sourceFile": "youtube/unlike.js", + "navigateBefore": "https://www.youtube.com" + }, + { + "site": "youtube", + "name": "unsubscribe", + "description": "Unsubscribe from a YouTube channel", + "domain": "www.youtube.com", + "strategy": "cookie", + "browser": true, + "args": [ + { + "name": "channel", + "type": "str", + "required": true, + "positional": true, + "help": "Channel ID (UCxxxx) or handle (@name)" + } + ], + "columns": [ + "status", + "message" + ], + "type": "js", + "modulePath": "youtube/unsubscribe.js", + "sourceFile": "youtube/unsubscribe.js", + "navigateBefore": "https://www.youtube.com" }, { "site": "youtube", @@ -16798,7 +17921,38 @@ ], "type": "js", "modulePath": "youtube/video.js", - "sourceFile": "youtube/video.js" + "sourceFile": "youtube/video.js", + "navigateBefore": "https://www.youtube.com" + }, + { + "site": "youtube", + "name": "watch-later", + "description": "Get your YouTube Watch Later queue", + "domain": "www.youtube.com", + "strategy": "cookie", + "browser": true, + "args": [ + { + "name": "limit", + "type": "int", + "default": 50, + "required": false, + "help": "Max videos to return (default 50, max 200)" + } + ], + "columns": [ + "rank", + "title", + "channel", + "duration", + "views", + "published", + "url" + ], + "type": "js", + "modulePath": "youtube/watch-later.js", + "sourceFile": "youtube/watch-later.js", + "navigateBefore": "https://www.youtube.com" }, { "site": "yuanbao", @@ -16911,7 +18065,8 @@ ], "type": "js", "modulePath": "zhihu/answer.js", - "sourceFile": "zhihu/answer.js" + "sourceFile": "zhihu/answer.js", + "navigateBefore": true }, { "site": "zhihu", @@ -16960,7 +18115,8 @@ ], "type": "js", "modulePath": "zhihu/comment.js", - "sourceFile": "zhihu/comment.js" + "sourceFile": "zhihu/comment.js", + "navigateBefore": true }, { "site": "zhihu", @@ -17000,7 +18156,8 @@ ], "type": "js", "modulePath": "zhihu/download.js", - "sourceFile": "zhihu/download.js" + "sourceFile": "zhihu/download.js", + "navigateBefore": "https://zhuanlan.zhihu.com" }, { "site": "zhihu", @@ -17047,7 +18204,8 @@ ], "type": "js", "modulePath": "zhihu/favorite.js", - "sourceFile": "zhihu/favorite.js" + "sourceFile": "zhihu/favorite.js", + "navigateBefore": true }, { "site": "zhihu", @@ -17080,7 +18238,8 @@ ], "type": "js", "modulePath": "zhihu/follow.js", - "sourceFile": "zhihu/follow.js" + "sourceFile": "zhihu/follow.js", + "navigateBefore": true }, { "site": "zhihu", @@ -17106,7 +18265,8 @@ ], "type": "js", "modulePath": "zhihu/hot.js", - "sourceFile": "zhihu/hot.js" + "sourceFile": "zhihu/hot.js", + "navigateBefore": "https://www.zhihu.com" }, { "site": "zhihu", @@ -17139,7 +18299,8 @@ ], "type": "js", "modulePath": "zhihu/like.js", - "sourceFile": "zhihu/like.js" + "sourceFile": "zhihu/like.js", + "navigateBefore": true }, { "site": "zhihu", @@ -17172,7 +18333,8 @@ ], "type": "js", "modulePath": "zhihu/question.js", - "sourceFile": "zhihu/question.js" + "sourceFile": "zhihu/question.js", + "navigateBefore": "https://www.zhihu.com" }, { "site": "zhihu", @@ -17207,7 +18369,8 @@ ], "type": "js", "modulePath": "zhihu/search.js", - "sourceFile": "zhihu/search.js" + "sourceFile": "zhihu/search.js", + "navigateBefore": "https://www.zhihu.com" }, { "site": "zsxq", @@ -17236,7 +18399,8 @@ ], "type": "js", "modulePath": "zsxq/dynamics.js", - "sourceFile": "zsxq/dynamics.js" + "sourceFile": "zsxq/dynamics.js", + "navigateBefore": "https://wx.zsxq.com" }, { "site": "zsxq", @@ -17265,7 +18429,8 @@ ], "type": "js", "modulePath": "zsxq/groups.js", - "sourceFile": "zsxq/groups.js" + "sourceFile": "zsxq/groups.js", + "navigateBefore": "https://wx.zsxq.com" }, { "site": "zsxq", @@ -17308,7 +18473,8 @@ ], "type": "js", "modulePath": "zsxq/search.js", - "sourceFile": "zsxq/search.js" + "sourceFile": "zsxq/search.js", + "navigateBefore": "https://wx.zsxq.com" }, { "site": "zsxq", @@ -17325,6 +18491,12 @@ "positional": true, "help": "Topic ID" }, + { + "name": "group_id", + "type": "str", + "required": false, + "help": "Group ID (optional; defaults to active group in Chrome)" + }, { "name": "comment_limit", "type": "int", @@ -17345,7 +18517,8 @@ ], "type": "js", "modulePath": "zsxq/topic.js", - "sourceFile": "zsxq/topic.js" + "sourceFile": "zsxq/topic.js", + "navigateBefore": "https://wx.zsxq.com" }, { "site": "zsxq", @@ -17381,6 +18554,7 @@ ], "type": "js", "modulePath": "zsxq/topics.js", - "sourceFile": "zsxq/topics.js" + "sourceFile": "zsxq/topics.js", + "navigateBefore": "https://wx.zsxq.com" } -] +] \ No newline at end of file diff --git a/clis/youtube/feed.js b/clis/youtube/feed.js new file mode 100644 index 000000000..37b73a99e --- /dev/null +++ b/clis/youtube/feed.js @@ -0,0 +1,120 @@ +/** + * YouTube feed — homepage recommended videos. + * Reads ytInitialData from the homepage directly (personalized, no separate API call needed). + */ +import { cli, Strategy } from '@jackwener/opencli/registry'; +import { CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors'; + +cli({ + site: 'youtube', + name: 'feed', + description: 'Get YouTube homepage recommended videos', + domain: 'www.youtube.com', + strategy: Strategy.COOKIE, + args: [ + { name: 'limit', type: 'int', default: 20, help: 'Max videos to return (default 20, max 100)' }, + ], + columns: ['rank', 'title', 'channel', 'views', 'duration', 'published', 'url'], + func: async (page, kwargs) => { + const limit = Math.min(kwargs.limit || 20, 100); + await page.goto('https://www.youtube.com'); + await page.wait(3); + const data = await page.evaluate(` + (async () => { + const d = window.ytInitialData; + if (!d) return { error: 'YouTube data not found — are you logged in?' }; + + const limit = ${limit}; + const cfg = window.ytcfg?.data_ || {}; + const apiKey = cfg.INNERTUBE_API_KEY; + const context = cfg.INNERTUBE_CONTEXT; + + function extractFromItem(item) { + // Modern lockupViewModel format + const lvm = item.richItemRenderer?.content?.lockupViewModel; + if (lvm && lvm.contentType === 'LOCKUP_CONTENT_TYPE_VIDEO') { + const meta = lvm.metadata?.lockupMetadataViewModel; + const rows = meta?.metadata?.contentMetadataViewModel?.metadataRows || []; + const parts = rows.flatMap(r => (r.metadataParts || []).map(p => p.text?.content || '').filter(Boolean)); + let duration = ''; + for (const ov of (lvm.contentImage?.thumbnailViewModel?.overlays || [])) { + for (const b of (ov.thumbnailBottomOverlayViewModel?.badges || [])) { + if (b.thumbnailBadgeViewModel?.text) duration = b.thumbnailBadgeViewModel.text; + } + } + return { + title: meta?.title?.content || '', + channel: parts[0] || '', + views: parts[1] || '', + duration, + published: parts[2] || '', + videoId: lvm.contentId, + }; + } + + // Legacy videoRenderer format + const v = item.richItemRenderer?.content?.videoRenderer || item.videoRenderer; + if (v?.videoId) { + return { + title: v.title?.runs?.[0]?.text || '', + channel: v.ownerText?.runs?.[0]?.text || v.shortBylineText?.runs?.[0]?.text || '', + views: v.viewCountText?.simpleText || v.shortViewCountText?.simpleText || '', + duration: v.lengthText?.simpleText || '', + published: v.publishedTimeText?.simpleText || '', + videoId: v.videoId, + }; + } + return null; + } + + const tabs = d.contents?.twoColumnBrowseResultsRenderer?.tabs || []; + const richContents = tabs[0]?.tabRenderer?.content?.richGridRenderer?.contents || []; + + const videos = []; + for (const item of richContents) { + if (videos.length >= limit) break; + const v = extractFromItem(item); + if (v?.videoId) { + videos.push({ rank: videos.length + 1, ...v, url: 'https://www.youtube.com/watch?v=' + v.videoId }); + } + } + + // Pagination + if (videos.length < limit && apiKey && context) { + let contItem = richContents[richContents.length - 1]; + while (videos.length < limit && contItem?.continuationItemRenderer) { + const token = contItem.continuationItemRenderer?.continuationEndpoint?.continuationCommand?.token; + if (!token) break; + const resp = await fetch('/youtubei/v1/browse?key=' + apiKey + '&prettyPrint=false', { + method: 'POST', credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ context, continuation: token }), + }); + if (!resp.ok) break; + const contData = await resp.json(); + const newItems = contData.onResponseReceivedActions?.[0]?.appendContinuationItemsAction?.continuationItems || []; + if (!newItems.length) break; + for (const item of newItems) { + if (videos.length >= limit) break; + const v = extractFromItem(item); + if (v?.videoId) { + videos.push({ rank: videos.length + 1, ...v, url: 'https://www.youtube.com/watch?v=' + v.videoId }); + } + } + contItem = newItems[newItems.length - 1]; + } + } + + return videos; + })() + `); + if (!Array.isArray(data)) { + const errMsg = data && typeof data === 'object' ? String(data.error || '') : ''; + throw new CommandExecutionError(errMsg || 'Failed to fetch YouTube feed'); + } + if (data.length === 0) { + throw new EmptyResultError('youtube feed'); + } + return data; + }, +}); diff --git a/clis/youtube/history.js b/clis/youtube/history.js new file mode 100644 index 000000000..f72f1237c --- /dev/null +++ b/clis/youtube/history.js @@ -0,0 +1,118 @@ +/** + * YouTube history — watch history via InnerTube browse API (FEhistory). + */ +import { cli, Strategy } from '@jackwener/opencli/registry'; +import { CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors'; + +cli({ + site: 'youtube', + name: 'history', + description: 'Get YouTube watch history', + domain: 'www.youtube.com', + strategy: Strategy.COOKIE, + args: [ + { name: 'limit', type: 'int', default: 30, help: 'Max videos to return (default 30, max 200)' }, + ], + columns: ['rank', 'title', 'channel', 'views', 'duration', 'url'], + func: async (page, kwargs) => { + const limit = Math.min(kwargs.limit || 30, 200); + await page.goto('https://www.youtube.com/feed/history'); + await page.wait(3); + await page.autoScroll({ times: Math.min(Math.max(Math.ceil(limit / 20), 1), 8), delayMs: 1200 }); + const data = await page.evaluate(` + (async () => { + const limit = ${limit}; + + const videos = []; + const seen = new Set(); + const root = document.querySelector('ytd-two-column-browse-results-renderer #primary ytd-section-list-renderer'); + if (!root) return { error: 'YouTube history list not found' }; + + function text(el) { + return (el?.textContent || '').replace(/\\s+/g, ' ').trim(); + } + + function push(entry) { + if (!entry?.url || seen.has(entry.url) || videos.length >= limit) return; + seen.add(entry.url); + videos.push({ rank: videos.length + 1, ...entry }); + } + + for (const section of root.querySelectorAll('ytd-item-section-renderer')) { + if (videos.length >= limit) break; + + for (const renderer of section.querySelectorAll('yt-lockup-view-model, ytd-video-renderer, ytd-rich-item-renderer, ytd-grid-video-renderer, ytd-compact-video-renderer')) { + if (videos.length >= limit) break; + const link = renderer.querySelector('a[href^="/watch?v="]'); + const href = link?.getAttribute('href') || ''; + if (!href) continue; + const title = + link?.getAttribute('title') + || text(renderer.querySelector('#video-title')) + || text(renderer.querySelector('h3 a')) + || text(renderer.querySelector('h3')) + || text(link); + const channel = + text(renderer.querySelector('#channel-name a')) + || text(renderer.querySelector('[aria-label^="前往频道:"]')) + || text(renderer.querySelector('[aria-label^="Go to channel:"]')) + || text(renderer.querySelector('ytd-channel-name')) + || text(renderer.querySelector('#metadata #byline-container')) + || ''; + const metadata = Array.from(renderer.querySelectorAll('#metadata-line span, #metadata span, .metadata span')) + .map(node => text(node)) + .filter(Boolean); + const lockupMetadata = Array.from(renderer.querySelectorAll('yt-content-metadata-view-model span, yt-lockup-metadata-view-model span')) + .map(node => text(node)) + .filter(Boolean); + const combinedMetadata = (metadata.length ? metadata : lockupMetadata) + .filter(value => value && value !== title && value !== '•'); + const inferredChannel = channel || combinedMetadata.find(value => !/观看|views|前|前に|ago|次观看|次查看|stream/i.test(value)) || ''; + const inferredViews = combinedMetadata.find(value => /观看|views/i.test(value)) || ''; + const inferredPublished = combinedMetadata.find(value => value !== inferredChannel && value !== inferredViews) || ''; + const duration = + text(renderer.querySelector('ytd-thumbnail-overlay-time-status-renderer')) + || text(renderer.querySelector('yt-thumbnail-badge-view-model')) + || text(renderer.querySelector('badge-shape')) + || ''; + push({ + title, + channel: inferredChannel, + views: inferredViews, + duration, + published: inferredPublished, + url: href.startsWith('http') ? href : 'https://www.youtube.com' + href, + }); + } + + for (const shortLink of section.querySelectorAll('a[href^="/shorts/"]')) { + if (videos.length >= limit) break; + const card = shortLink.closest('ytm-shorts-lockup-view-model-v2, ytm-shorts-lockup-view-model, ytd-reel-item-renderer') || shortLink.parentElement; + const href = shortLink.getAttribute('href') || ''; + if (!href) continue; + const title = shortLink.getAttribute('title') || text(card?.querySelector('h3')) || text(shortLink); + const stats = Array.from(card?.querySelectorAll('span') || []).map(node => text(node)).filter(Boolean); + push({ + title, + channel: 'Shorts', + views: stats.find(value => /观看|views/i.test(value)) || '', + duration: 'SHORT', + published: '', + url: href.startsWith('http') ? href : 'https://www.youtube.com' + href, + }); + } + } + + return videos.length ? videos : { error: 'No watch history items found on youtube.com/feed/history' }; + })() + `); + if (!Array.isArray(data)) { + const errMsg = data && typeof data === 'object' ? String(data.error || '') : ''; + throw new CommandExecutionError(errMsg || 'Failed to fetch watch history — make sure you are logged into YouTube'); + } + if (data.length === 0) { + throw new EmptyResultError('youtube history'); + } + return data; + }, +}); diff --git a/clis/youtube/like.js b/clis/youtube/like.js new file mode 100644 index 000000000..b9da0537a --- /dev/null +++ b/clis/youtube/like.js @@ -0,0 +1,62 @@ +/** + * YouTube like — like a video via InnerTube like API (requires SAPISIDHASH auth). + */ +import { cli, Strategy } from '@jackwener/opencli/registry'; +import { parseVideoId, prepareYoutubeApiPage, SAPISID_HASH_FN } from './utils.js'; +import { CommandExecutionError, AuthRequiredError } from '@jackwener/opencli/errors'; + +cli({ + site: 'youtube', + name: 'like', + description: 'Like a YouTube video', + domain: 'www.youtube.com', + strategy: Strategy.COOKIE, + args: [ + { name: 'url', required: true, positional: true, help: 'YouTube video URL or video ID' }, + ], + columns: ['status', 'message'], + func: async (page, kwargs) => { + const videoId = parseVideoId(String(kwargs.url)); + await prepareYoutubeApiPage(page); + const result = await page.evaluate(` + (async () => { + ${SAPISID_HASH_FN} + + const cfg = window.ytcfg?.data_ || {}; + const apiKey = cfg.INNERTUBE_API_KEY; + const context = cfg.INNERTUBE_CONTEXT; + if (!apiKey || !context) return { error: 'config', message: 'YouTube config not found' }; + + const authHash = await getSapisidHash('https://www.youtube.com'); + if (!authHash) return { error: 'auth', message: 'Not logged in (SAPISID cookie missing)' }; + + const resp = await fetch('/youtubei/v1/like/like?key=' + apiKey + '&prettyPrint=false', { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + 'Authorization': authHash, + 'X-Origin': 'https://www.youtube.com', + }, + body: JSON.stringify({ context, target: { videoId: ${JSON.stringify(videoId)} } }), + }); + + if (resp.status === 401 || resp.status === 403) return { error: 'auth', message: 'Not logged in' }; + if (!resp.ok) { + const body = await resp.json().catch(() => ({})); + const errStatus = body?.error?.status || ''; + if (errStatus === 'UNAUTHENTICATED') return { error: 'auth', message: 'Not logged in' }; + return { error: 'http', message: 'HTTP ' + resp.status + (errStatus ? ' ' + errStatus : '') }; + } + return { ok: true }; + })() + `); + if (result?.error === 'auth') { + throw new AuthRequiredError('www.youtube.com'); + } + if (result?.error) { + throw new CommandExecutionError(result.message || 'Failed to like video'); + } + return [{ status: 'success', message: 'Liked: ' + videoId }]; + }, +}); diff --git a/clis/youtube/playlist.js b/clis/youtube/playlist.js new file mode 100644 index 000000000..3f4088294 --- /dev/null +++ b/clis/youtube/playlist.js @@ -0,0 +1,97 @@ +/** + * YouTube playlist — get playlist info and video list via InnerTube browse API. + */ +import { cli, Strategy } from '@jackwener/opencli/registry'; +import { prepareYoutubeApiPage, FETCH_BROWSE_FN, extractPlaylistVideos } from './utils.js'; +import { CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors'; + +/** + * Parse a playlist ID from a URL or bare ID string. + */ +function parsePlaylistId(input) { + if (!input.startsWith('http')) + return input; + try { + const url = new URL(input); + return url.searchParams.get('list') || input; + } + catch { + return input; + } +} + +cli({ + site: 'youtube', + name: 'playlist', + description: 'Get YouTube playlist info and video list', + domain: 'www.youtube.com', + strategy: Strategy.COOKIE, + args: [ + { name: 'id', required: true, positional: true, help: 'Playlist URL or playlist ID (PLxxxxxx)' }, + { name: 'limit', type: 'int', default: 50, help: 'Max videos to return (default 50, max 200)' }, + ], + columns: ['rank', 'title', 'channel', 'duration', 'views', 'published', 'url'], + func: async (page, kwargs) => { + const playlistId = parsePlaylistId(String(kwargs.id)); + const limit = Math.min(kwargs.limit || 50, 200); + await prepareYoutubeApiPage(page); + const data = await page.evaluate(` + (async () => { + const cfg = window.ytcfg?.data_ || {}; + const apiKey = cfg.INNERTUBE_API_KEY; + const context = cfg.INNERTUBE_CONTEXT; + if (!apiKey || !context) return { error: 'YouTube config not found' }; + + const browseId = 'VL' + ${JSON.stringify(playlistId)}; + const limit = ${limit}; + + ${FETCH_BROWSE_FN} + + const data = await fetchBrowse(apiKey, { context, browseId }); + if (data.error) return data; + + const header = data.header?.pageHeaderRenderer; + const title = header?.pageTitle || ''; + const metaRows = header?.content?.pageHeaderViewModel?.metadata?.contentMetadataViewModel?.metadataRows || []; + const stats = metaRows.flatMap(r => (r.metadataParts || []).map(p => p.text?.content || '').filter(Boolean)); + + const sidebarItems = data.sidebar?.playlistSidebarRenderer?.items || []; + const secondaryInfo = sidebarItems.find(i => i.playlistSidebarSecondaryInfoRenderer)?.playlistSidebarSecondaryInfoRenderer; + const channelName = secondaryInfo?.videoOwner?.videoOwnerRenderer?.title?.runs?.[0]?.text || ''; + + const tabs = data.contents?.twoColumnBrowseResultsRenderer?.tabs || []; + let listContents = tabs[0]?.tabRenderer?.content?.sectionListRenderer?.contents?.[0]?.itemSectionRenderer?.contents?.[0]?.playlistVideoListRenderer?.contents || []; + + const extractVideos = ${extractPlaylistVideos.toString()}; + + let videos = extractVideos(listContents); + + let contItem = listContents[listContents.length - 1]; + while (videos.length < limit && contItem?.continuationItemRenderer) { + const token = contItem.continuationItemRenderer?.continuationEndpoint?.continuationCommand?.token; + if (!token) break; + const contData = await fetchBrowse(apiKey, { context, continuation: token }); + if (contData.error) break; + const newItems = contData.onResponseReceivedActions?.[0]?.appendContinuationItemsAction?.continuationItems || []; + if (!newItems.length) break; + videos = videos.concat(extractVideos(newItems)); + contItem = newItems[newItems.length - 1]; + } + + return { title, channelName, stats, videos: videos.slice(0, limit) }; + })() + `); + if (!data || typeof data !== 'object') { + throw new CommandExecutionError('Failed to fetch playlist data'); + } + if (data.error) { + throw new CommandExecutionError(String(data.error)); + } + if (!data.videos?.length) { + throw new EmptyResultError('youtube playlist'); + } + const statsStr = (data.stats || []).join(' | '); + process.stderr.write(`${data.title} [${data.channelName}] ${statsStr}\n`); + return data.videos; + }, +}); diff --git a/clis/youtube/subscribe.js b/clis/youtube/subscribe.js new file mode 100644 index 000000000..03575aae3 --- /dev/null +++ b/clis/youtube/subscribe.js @@ -0,0 +1,71 @@ +/** + * YouTube subscribe — subscribe to a channel via InnerTube subscription API. + */ +import { cli, Strategy } from '@jackwener/opencli/registry'; +import { prepareYoutubeApiPage, SAPISID_HASH_FN, RESOLVE_CHANNEL_HANDLE_FN } from './utils.js'; +import { CommandExecutionError, AuthRequiredError } from '@jackwener/opencli/errors'; + +cli({ + site: 'youtube', + name: 'subscribe', + description: 'Subscribe to a YouTube channel', + domain: 'www.youtube.com', + strategy: Strategy.COOKIE, + args: [ + { name: 'channel', required: true, positional: true, help: 'Channel ID (UCxxxx) or handle (@name)' }, + ], + columns: ['status', 'message'], + func: async (page, kwargs) => { + const channelInput = String(kwargs.channel); + await prepareYoutubeApiPage(page); + const result = await page.evaluate(` + (async () => { + ${SAPISID_HASH_FN} + + const cfg = window.ytcfg?.data_ || {}; + const apiKey = cfg.INNERTUBE_API_KEY; + const context = cfg.INNERTUBE_CONTEXT; + if (!apiKey || !context) return { error: 'config', message: 'YouTube config not found' }; + + const authHash = await getSapisidHash('https://www.youtube.com'); + if (!authHash) return { error: 'auth', message: 'Not logged in (SAPISID cookie missing)' }; + + ${RESOLVE_CHANNEL_HANDLE_FN} + + let channelId = ${JSON.stringify(channelInput)}; + channelId = await resolveChannelHandle(channelId, apiKey, context); + + if (!channelId.startsWith('UC')) { + return { error: 'arg', message: 'Could not resolve channel ID from: ' + ${JSON.stringify(channelInput)} }; + } + + const resp = await fetch('/youtubei/v1/subscription/subscribe?key=' + apiKey + '&prettyPrint=false', { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + 'Authorization': authHash, + 'X-Origin': 'https://www.youtube.com', + }, + body: JSON.stringify({ context, channelIds: [channelId] }), + }); + + if (resp.status === 401 || resp.status === 403) return { error: 'auth', message: 'Not logged in' }; + if (!resp.ok) { + const body = await resp.json().catch(() => ({})); + const errStatus = body?.error?.status || ''; + if (errStatus === 'UNAUTHENTICATED') return { error: 'auth', message: 'Not logged in' }; + return { error: 'http', message: 'HTTP ' + resp.status + (errStatus ? ' ' + errStatus : '') }; + } + return { ok: true, channelId }; + })() + `); + if (result?.error === 'auth') { + throw new AuthRequiredError('www.youtube.com'); + } + if (result?.error) { + throw new CommandExecutionError(result.message || 'Failed to subscribe'); + } + return [{ status: 'success', message: 'Subscribed to: ' + (result.channelId || channelInput) }]; + }, +}); diff --git a/clis/youtube/subscriptions.js b/clis/youtube/subscriptions.js new file mode 100644 index 000000000..3f7b88c0d --- /dev/null +++ b/clis/youtube/subscriptions.js @@ -0,0 +1,57 @@ +/** + * YouTube subscriptions — list of subscribed channels from /feed/channels. + */ +import { cli, Strategy } from '@jackwener/opencli/registry'; +import { CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors'; +import { extractSubscriptionChannel } from './utils.js'; + +cli({ + site: 'youtube', + name: 'subscriptions', + description: 'List subscribed YouTube channels', + domain: 'www.youtube.com', + strategy: Strategy.COOKIE, + args: [ + { name: 'limit', type: 'int', default: 50, help: 'Max channels to return (default 50)' }, + ], + columns: ['rank', 'name', 'handle', 'subscribers', 'url'], + func: async (page, kwargs) => { + const limit = Math.min(kwargs.limit || 50, 1000); + await page.goto('https://www.youtube.com/feed/channels'); + await page.wait(3); + const data = await page.evaluate(` + (async () => { + const d = window.ytInitialData; + if (!d) return { error: 'YouTube data not found — are you logged in?' }; + + const limit = ${limit}; + + const items = d.contents?.twoColumnBrowseResultsRenderer + ?.tabs?.[0]?.tabRenderer?.content + ?.sectionListRenderer?.contents?.[0] + ?.itemSectionRenderer?.contents?.[0] + ?.shelfRenderer?.content + ?.expandedShelfContentsRenderer?.items || []; + + const extractChannel = ${extractSubscriptionChannel.toString()}; + + const channels = []; + for (const item of items) { + if (channels.length >= limit) break; + const ch = extractChannel(item.channelRenderer); + if (ch?.name) channels.push(ch); + } + + return channels; + })() + `); + if (!Array.isArray(data)) { + const errMsg = data && typeof data === 'object' ? String(data.error || '') : ''; + throw new CommandExecutionError(errMsg || 'Failed to fetch subscriptions — make sure you are logged into YouTube'); + } + if (data.length === 0) { + throw new EmptyResultError('youtube subscriptions'); + } + return data.map((ch, i) => ({ rank: i + 1, ...ch })); + }, +}); diff --git a/clis/youtube/unlike.js b/clis/youtube/unlike.js new file mode 100644 index 000000000..a6f3fef53 --- /dev/null +++ b/clis/youtube/unlike.js @@ -0,0 +1,62 @@ +/** + * YouTube unlike — remove like from a video via InnerTube like API. + */ +import { cli, Strategy } from '@jackwener/opencli/registry'; +import { parseVideoId, prepareYoutubeApiPage, SAPISID_HASH_FN } from './utils.js'; +import { CommandExecutionError, AuthRequiredError } from '@jackwener/opencli/errors'; + +cli({ + site: 'youtube', + name: 'unlike', + description: 'Remove like from a YouTube video', + domain: 'www.youtube.com', + strategy: Strategy.COOKIE, + args: [ + { name: 'url', required: true, positional: true, help: 'YouTube video URL or video ID' }, + ], + columns: ['status', 'message'], + func: async (page, kwargs) => { + const videoId = parseVideoId(String(kwargs.url)); + await prepareYoutubeApiPage(page); + const result = await page.evaluate(` + (async () => { + ${SAPISID_HASH_FN} + + const cfg = window.ytcfg?.data_ || {}; + const apiKey = cfg.INNERTUBE_API_KEY; + const context = cfg.INNERTUBE_CONTEXT; + if (!apiKey || !context) return { error: 'config', message: 'YouTube config not found' }; + + const authHash = await getSapisidHash('https://www.youtube.com'); + if (!authHash) return { error: 'auth', message: 'Not logged in (SAPISID cookie missing)' }; + + const resp = await fetch('/youtubei/v1/like/removelike?key=' + apiKey + '&prettyPrint=false', { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + 'Authorization': authHash, + 'X-Origin': 'https://www.youtube.com', + }, + body: JSON.stringify({ context, target: { videoId: ${JSON.stringify(videoId)} } }), + }); + + if (resp.status === 401 || resp.status === 403) return { error: 'auth', message: 'Not logged in' }; + if (!resp.ok) { + const body = await resp.json().catch(() => ({})); + const errStatus = body?.error?.status || ''; + if (errStatus === 'UNAUTHENTICATED') return { error: 'auth', message: 'Not logged in' }; + return { error: 'http', message: 'HTTP ' + resp.status + (errStatus ? ' ' + errStatus : '') }; + } + return { ok: true }; + })() + `); + if (result?.error === 'auth') { + throw new AuthRequiredError('www.youtube.com'); + } + if (result?.error) { + throw new CommandExecutionError(result.message || 'Failed to remove like'); + } + return [{ status: 'success', message: 'Unliked: ' + videoId }]; + }, +}); diff --git a/clis/youtube/unsubscribe.js b/clis/youtube/unsubscribe.js new file mode 100644 index 000000000..f02b290e5 --- /dev/null +++ b/clis/youtube/unsubscribe.js @@ -0,0 +1,71 @@ +/** + * YouTube unsubscribe — unsubscribe from a channel via InnerTube subscription API. + */ +import { cli, Strategy } from '@jackwener/opencli/registry'; +import { prepareYoutubeApiPage, SAPISID_HASH_FN, RESOLVE_CHANNEL_HANDLE_FN } from './utils.js'; +import { CommandExecutionError, AuthRequiredError } from '@jackwener/opencli/errors'; + +cli({ + site: 'youtube', + name: 'unsubscribe', + description: 'Unsubscribe from a YouTube channel', + domain: 'www.youtube.com', + strategy: Strategy.COOKIE, + args: [ + { name: 'channel', required: true, positional: true, help: 'Channel ID (UCxxxx) or handle (@name)' }, + ], + columns: ['status', 'message'], + func: async (page, kwargs) => { + const channelInput = String(kwargs.channel); + await prepareYoutubeApiPage(page); + const result = await page.evaluate(` + (async () => { + ${SAPISID_HASH_FN} + + const cfg = window.ytcfg?.data_ || {}; + const apiKey = cfg.INNERTUBE_API_KEY; + const context = cfg.INNERTUBE_CONTEXT; + if (!apiKey || !context) return { error: 'config', message: 'YouTube config not found' }; + + const authHash = await getSapisidHash('https://www.youtube.com'); + if (!authHash) return { error: 'auth', message: 'Not logged in (SAPISID cookie missing)' }; + + ${RESOLVE_CHANNEL_HANDLE_FN} + + let channelId = ${JSON.stringify(channelInput)}; + channelId = await resolveChannelHandle(channelId, apiKey, context); + + if (!channelId.startsWith('UC')) { + return { error: 'arg', message: 'Could not resolve channel ID from: ' + ${JSON.stringify(channelInput)} }; + } + + const resp = await fetch('/youtubei/v1/subscription/unsubscribe?key=' + apiKey + '&prettyPrint=false', { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + 'Authorization': authHash, + 'X-Origin': 'https://www.youtube.com', + }, + body: JSON.stringify({ context, channelIds: [channelId] }), + }); + + if (resp.status === 401 || resp.status === 403) return { error: 'auth', message: 'Not logged in' }; + if (!resp.ok) { + const body = await resp.json().catch(() => ({})); + const errStatus = body?.error?.status || ''; + if (errStatus === 'UNAUTHENTICATED') return { error: 'auth', message: 'Not logged in' }; + return { error: 'http', message: 'HTTP ' + resp.status + (errStatus ? ' ' + errStatus : '') }; + } + return { ok: true, channelId }; + })() + `); + if (result?.error === 'auth') { + throw new AuthRequiredError('www.youtube.com'); + } + if (result?.error) { + throw new CommandExecutionError(result.message || 'Failed to unsubscribe'); + } + return [{ status: 'success', message: 'Unsubscribed from: ' + (result.channelId || channelInput) }]; + }, +}); diff --git a/clis/youtube/utils.js b/clis/youtube/utils.js index a70934a31..7db40926b 100644 --- a/clis/youtube/utils.js +++ b/clis/youtube/utils.js @@ -90,3 +90,125 @@ export async function prepareYoutubeApiPage(page) { await page.goto('https://www.youtube.com', { waitUntil: 'none' }); await page.wait(2); } +/** + * Inline InnerTube browse API helper for use inside page.evaluate() strings. + * Inject via FETCH_BROWSE_FN, then call: fetchBrowse(apiKey, body) + */ +export const FETCH_BROWSE_FN = ` +async function fetchBrowse(apiKey, body) { + const resp = await fetch('/youtubei/v1/browse?key=' + apiKey + '&prettyPrint=false', { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + if (!resp.ok) return { error: 'InnerTube browse API returned HTTP ' + resp.status }; + return resp.json(); +} +`; +/** + * Extract video objects from playlistVideoRenderer items (playlists, watch-later). + * Pure function — inject into page.evaluate() via: extractPlaylistVideos.toString() + */ +export function extractPlaylistVideos(items) { + return items + .filter(i => i.playlistVideoRenderer) + .map(i => { + const v = i.playlistVideoRenderer; + const infoRuns = v.videoInfo?.runs || []; + return { + rank: parseInt(v.index?.simpleText || '0', 10), + title: v.title?.runs?.[0]?.text || '', + channel: v.shortBylineText?.runs?.[0]?.text || '', + duration: v.lengthText?.simpleText || '', + views: infoRuns[0]?.text || '', + published: infoRuns[2]?.text || '', + url: 'https://www.youtube.com/watch?v=' + v.videoId, + }; + }); +} +/** + * Normalize a subscribed channel entry from YouTube's channelRenderer payload. + * Different surfaces/locales may expose the handle in channelHandleText, canonicalBaseUrl, + * or, in some variants, overload one of the count fields with an @handle string. + */ +export function extractSubscriptionChannel(channelRenderer) { + const readText = (value) => { + if (!value) + return ''; + if (typeof value.simpleText === 'string') + return value.simpleText.trim(); + if (Array.isArray(value.runs)) { + return value.runs + .map((run) => run?.text || '') + .join('') + .trim(); + } + return ''; + }; + const ch = channelRenderer || {}; + const name = readText(ch.title); + const baseUrl = ch.navigationEndpoint?.browseEndpoint?.canonicalBaseUrl || ''; + const channelId = ch.channelId || ch.navigationEndpoint?.browseEndpoint?.browseId || ''; + const subscriberCountText = readText(ch.subscriberCountText); + const videoCountText = readText(ch.videoCountText); + const handle = [ + readText(ch.channelHandleText), + baseUrl.startsWith('/@') ? baseUrl.slice(1) : '', + subscriberCountText.startsWith('@') ? subscriberCountText : '', + videoCountText.startsWith('@') ? videoCountText : '', + ].find(Boolean) || ''; + const subscribers = [ + !subscriberCountText.startsWith('@') ? subscriberCountText : '', + !videoCountText.startsWith('@') ? videoCountText : '', + ].find(Boolean) || ''; + const url = baseUrl + ? 'https://www.youtube.com' + baseUrl + : channelId ? 'https://www.youtube.com/channel/' + channelId : ''; + return { name, handle, subscribers, url }; +} +/** + * Inline @handle → channelId resolver for use inside page.evaluate() strings. + * Inject via RESOLVE_CHANNEL_HANDLE_FN, then call: resolveChannelHandle(input, apiKey, context) + */ +export const RESOLVE_CHANNEL_HANDLE_FN = ` +async function resolveChannelHandle(input, apiKey, context) { + if (!input.startsWith('@')) return input; + const resp = await fetch('/youtubei/v1/navigation/resolve_url?key=' + apiKey + '&prettyPrint=false', { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ context, url: 'https://www.youtube.com/' + input }), + }); + if (!resp.ok) return input; + const data = await resp.json().catch(() => ({})); + return data.endpoint?.browseEndpoint?.browseId || input; +} +`; +/** + * Inline SAPISIDHASH helper for use inside page.evaluate() strings. + * YouTube write APIs (like, subscribe) require: + * Authorization: SAPISIDHASH {time}_{SHA1(time + " " + SAPISID + " " + origin)} + */ +export const SAPISID_HASH_FN = ` +async function getSapisidHash(origin) { + const cookies = document.cookie.split('; '); + let sapisid = ''; + for (const c of cookies) { + const eq = c.indexOf('='); + if (eq === -1) continue; + const name = c.slice(0, eq); + const val = c.slice(eq + 1); + if (name === '__Secure-3PAPISID' || name === 'SAPISID') { + sapisid = val; + if (name === '__Secure-3PAPISID') break; + } + } + if (!sapisid) return null; + const time = Math.floor(Date.now() / 1000); + const msgBuffer = new TextEncoder().encode(time + ' ' + sapisid + ' ' + origin); + const hashBuffer = await crypto.subtle.digest('SHA-1', msgBuffer); + const hashHex = Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join(''); + return 'SAPISIDHASH ' + time + '_' + hashHex; +} +`; diff --git a/clis/youtube/utils.test.js b/clis/youtube/utils.test.js index 36f5272c8..886188d45 100644 --- a/clis/youtube/utils.test.js +++ b/clis/youtube/utils.test.js @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest'; -import { extractJsonAssignmentFromHtml, prepareYoutubeApiPage } from './utils.js'; +import { extractJsonAssignmentFromHtml, extractSubscriptionChannel, prepareYoutubeApiPage } from './utils.js'; describe('youtube utils', () => { it('extractJsonAssignmentFromHtml parses bootstrap objects with nested braces in strings', () => { const html = ` @@ -34,4 +34,35 @@ describe('youtube utils', () => { expect(page.goto).toHaveBeenCalledWith('https://www.youtube.com', { waitUntil: 'none' }); expect(page.wait).toHaveBeenCalledWith(2); }); + it('extractSubscriptionChannel prefers explicit handle and subscriber count fields', () => { + expect(extractSubscriptionChannel({ + title: { simpleText: 'OpenAI' }, + channelHandleText: { runs: [{ text: '@openai' }] }, + subscriberCountText: { simpleText: '1.23M subscribers' }, + videoCountText: { simpleText: '123 videos' }, + navigationEndpoint: { browseEndpoint: { canonicalBaseUrl: '/channel/UC123' } }, + channelId: 'UC123', + })).toEqual({ + name: 'OpenAI', + handle: '@openai', + subscribers: '1.23M subscribers', + url: 'https://www.youtube.com/channel/UC123', + }); + }); + it('extractSubscriptionChannel falls back when handle/count fields are overloaded', () => { + expect(extractSubscriptionChannel({ + title: { + runs: [{ text: 'OpenAI' }], + }, + subscriberCountText: { simpleText: '@openai' }, + videoCountText: { simpleText: '1.23M subscribers' }, + navigationEndpoint: { browseEndpoint: { canonicalBaseUrl: '/@openai' } }, + channelId: 'UC123', + })).toEqual({ + name: 'OpenAI', + handle: '@openai', + subscribers: '1.23M subscribers', + url: 'https://www.youtube.com/@openai', + }); + }); }); diff --git a/clis/youtube/watch-later.js b/clis/youtube/watch-later.js new file mode 100644 index 000000000..8c229eb1b --- /dev/null +++ b/clis/youtube/watch-later.js @@ -0,0 +1,76 @@ +/** + * YouTube watch-later — the user's Watch Later queue. + * Navigates to /playlist?list=WL and reads ytInitialData directly. + */ +import { cli, Strategy } from '@jackwener/opencli/registry'; +import { FETCH_BROWSE_FN, extractPlaylistVideos } from './utils.js'; +import { CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors'; + +cli({ + site: 'youtube', + name: 'watch-later', + description: 'Get your YouTube Watch Later queue', + domain: 'www.youtube.com', + strategy: Strategy.COOKIE, + args: [ + { name: 'limit', type: 'int', default: 50, help: 'Max videos to return (default 50, max 200)' }, + ], + columns: ['rank', 'title', 'channel', 'duration', 'views', 'published', 'url'], + func: async (page, kwargs) => { + const limit = Math.min(kwargs.limit || 50, 200); + await page.goto('https://www.youtube.com/playlist?list=WL'); + await page.wait(3); + const data = await page.evaluate(` + (async () => { + const d = window.ytInitialData; + if (!d) return { error: 'YouTube data not found — are you logged in?' }; + + const limit = ${limit}; + const cfg = window.ytcfg?.data_ || {}; + const apiKey = cfg.INNERTUBE_API_KEY; + const context = cfg.INNERTUBE_CONTEXT; + + const header = d.header?.playlistHeaderRenderer; + const title = header?.title?.simpleText || 'Watch Later'; + const stats = (header?.stats || []) + .map(s => s.runs?.map(r => r.text)?.join('') || s.simpleText || '') + .filter(Boolean); + + const tabs = d.contents?.twoColumnBrowseResultsRenderer?.tabs || []; + let listContents = tabs[0]?.tabRenderer?.content?.sectionListRenderer?.contents?.[0]?.itemSectionRenderer?.contents?.[0]?.playlistVideoListRenderer?.contents || []; + + ${FETCH_BROWSE_FN} + + const extractVideos = ${extractPlaylistVideos.toString()}; + + let videos = extractVideos(listContents); + + let contItem = listContents[listContents.length - 1]; + while (videos.length < limit && contItem?.continuationItemRenderer && apiKey && context) { + const token = contItem.continuationItemRenderer?.continuationEndpoint?.continuationCommand?.token; + if (!token) break; + const contData = await fetchBrowse(apiKey, { context, continuation: token }); + if (contData.error) break; + const newItems = contData.onResponseReceivedActions?.[0]?.appendContinuationItemsAction?.continuationItems || []; + if (!newItems.length) break; + videos = videos.concat(extractVideos(newItems)); + contItem = newItems[newItems.length - 1]; + } + + return { title, stats, videos: videos.slice(0, limit) }; + })() + `); + if (!data || typeof data !== 'object') { + throw new CommandExecutionError('Failed to fetch Watch Later — make sure you are logged into YouTube'); + } + if (data.error) { + throw new CommandExecutionError(String(data.error)); + } + if (!data.videos?.length) { + throw new EmptyResultError('youtube watch-later'); + } + const statsStr = (data.stats || []).join(' | '); + process.stderr.write(`${data.title} ${statsStr}\n`); + return data.videos; + }, +}); diff --git a/docs/adapters/browser/youtube.md b/docs/adapters/browser/youtube.md index dfa8e8f8d..4a5565e0d 100644 --- a/docs/adapters/browser/youtube.md +++ b/docs/adapters/browser/youtube.md @@ -6,21 +6,40 @@ | Command | Description | |---------|-------------| -| `opencli youtube search` | | -| `opencli youtube video` | | -| `opencli youtube transcript` | | +| `opencli youtube search` | Search videos | +| `opencli youtube video` | Get video metadata | +| `opencli youtube transcript` | Get video transcript/subtitles | +| `opencli youtube comments` | Get video comments | +| `opencli youtube channel` | Get channel info and videos | +| `opencli youtube playlist` | Get playlist video list | +| `opencli youtube feed` | Homepage recommended videos | +| `opencli youtube history` | Watch history | +| `opencli youtube watch-later` | Watch Later queue | +| `opencli youtube subscriptions` | List subscribed channels | +| `opencli youtube like` | Like a video | +| `opencli youtube unlike` | Remove like from a video | +| `opencli youtube subscribe` | Subscribe to a channel | +| `opencli youtube unsubscribe` | Unsubscribe from a channel | ## Usage Examples ```bash -# Quick start -opencli youtube search --limit 5 - -# JSON output -opencli youtube search -f json - -# Verbose mode -opencli youtube search -v +# Read commands +opencli youtube feed --limit 10 +opencli youtube history --limit 20 +opencli youtube watch-later --limit 50 +opencli youtube subscriptions --limit 30 + +# Search and video info +opencli youtube search "rust programming" --limit 5 +opencli youtube video "https://www.youtube.com/watch?v=xxx" +opencli youtube transcript "https://www.youtube.com/watch?v=xxx" + +# Write commands (requires login) +opencli youtube like "https://www.youtube.com/watch?v=xxx" +opencli youtube unlike "videoId" +opencli youtube subscribe "@ChannelHandle" +opencli youtube unsubscribe "UCxxxxxxxxxxxxxx" ``` ## Prerequisites diff --git a/docs/adapters/index.md b/docs/adapters/index.md index b9eea8ac2..ab604a4eb 100644 --- a/docs/adapters/index.md +++ b/docs/adapters/index.md @@ -15,7 +15,7 @@ Run `opencli list` for the live registry. | **[xiaohongshu](./browser/xiaohongshu.md)** | `search` `notifications` `feed` `user` `note` `comments` `download` `publish` `creator-notes` `creator-note-detail` `creator-notes-summary` `creator-profile` `creator-stats` | 🔐 Browser | | **[xiaoe](./browser/xiaoe.md)** | `courses` `detail` `catalog` `play-url` `content` | 🔐 Browser | | **[xueqiu](./browser/xueqiu.md)** | `feed` `hot-stock` `hot` `search` `stock` `comments` `watchlist` `earnings-date` `fund-holdings` `fund-snapshot` | 🔐 Browser | -| **[youtube](./browser/youtube.md)** | `search` `video` `transcript` | 🔐 Browser | +| **[youtube](./browser/youtube.md)** | `search` `video` `transcript` `comments` `channel` `playlist` `feed` `history` `watch-later` `subscriptions` `like` `unlike` `subscribe` `unsubscribe` | 🔐 Browser | | **[v2ex](./browser/v2ex.md)** | `hot` `latest` `topic` `node` `user` `member` `replies` `nodes` `daily` `me` `notifications` | 🌐 / 🔐 | | **[bloomberg](./browser/bloomberg.md)** | `main` `markets` `economics` `industries` `tech` `politics` `businessweek` `opinions` `feeds` `news` | 🌐 / 🔐 | | **[weibo](./browser/weibo.md)** | `hot` `search` `feed` `user` `me` `post` `comments` | 🔐 Browser | diff --git a/skills/opencli-usage/SKILL.md b/skills/opencli-usage/SKILL.md index b67c07642..4aa30820a 100644 --- a/skills/opencli-usage/SKILL.md +++ b/skills/opencli-usage/SKILL.md @@ -130,7 +130,7 @@ Type legend: 🌐 = Browser (needs Chrome login) · ✅ = Public API (no browser | **xueqiu** | 🌐 | `hot-stock` `stock` `watchlist` `feed` `hot` `search` `comments` `earnings-date` `fund-holdings` `fund-snapshot` | | **yahoo-finance** | 🌐 | `quote` | | **yollomi** | 🌐 | `models` `generate` `video` `upload` `remove-bg` `edit` `background` `face-swap` `object-remover` `restore` `try-on` `upscale` | -| **youtube** | 🌐 | `search` `video` `transcript` | +| **youtube** | 🌐 | `search` `video` `transcript` `comments` `channel` `playlist` `feed` `history` `watch-later` `subscriptions` `like` `unlike` `subscribe` `unsubscribe` | | **yuanbao** | 🌐 | `new` `ask` | | **zhihu** | 🌐 | `hot` `search` `question` | | **zsxq** | 🌐 | `groups` `dynamics` `topics` `topic` `search` | diff --git a/skills/opencli-usage/commands.md b/skills/opencli-usage/commands.md index 56fdfa36e..38b011a6d 100644 --- a/skills/opencli-usage/commands.md +++ b/skills/opencli-usage/commands.md @@ -797,10 +797,18 @@ opencli yollomi upscale # AI 超分辨率 (1 credit, 支持 --s ## YouTube 🌐 ```bash +opencli youtube feed --limit 10 # 首页推荐 +opencli youtube history --limit 20 # 观看历史 +opencli youtube watch-later --limit 50 # 稍后再看 +opencli youtube subscriptions --limit 30 # 订阅频道列表 opencli youtube search "rust" # 搜索视频 (query positional) opencli youtube video "https://www.youtube.com/watch?v=xxx" # 视频元数据 opencli youtube transcript "https://www.youtube.com/watch?v=xxx" # 获取视频字幕/转录 opencli youtube transcript "xxx" --lang zh-Hans --mode raw # 指定语言 + 原始时间戳模式 +opencli youtube like "https://www.youtube.com/watch?v=xxx" # 点赞视频 +opencli youtube unlike "xxx" # 取消点赞 +opencli youtube subscribe "@OpenAI" # 订阅频道 +opencli youtube unsubscribe "UCxxxxxxxxxxxxxx" # 取消订阅 ``` ## Yuanbao (腾讯元宝) 🌐