diff --git a/lib/routes/ganjingworld/channel/articles.ts b/lib/routes/ganjingworld/channel/articles.ts new file mode 100644 index 000000000..857bdb583 --- /dev/null +++ b/lib/routes/ganjingworld/channel/articles.ts @@ -0,0 +1,73 @@ +// RSSHub route for fetching articles from Ganjing World. +// Returns a list of articles in a channel. +// Source: https://www.ganjingworld.com + +import type { Route } from '@/types'; +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; + +import type { ApiResponse } from '../interfaces/api'; + +export const route: Route = { + path: '/channel/articles/:id', + categories: ['social-media'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + example: '/ganjingworld/channel/articles/1fcahpcut9t3gz4zIvYSJR7qd1cs0c', + parameters: { id: 'Channel ID, can be found in channel url' }, + radar: [ + { + source: ['ganjingworld.com'], + target: '/channel/articles/:id', + }, + ], + url: 'www.ganjingworld.com', + name: 'Articles in a channel', + maintainers: ['yixiangli2001'], + + handler, +}; + +async function handler(ctx) { + const id = ctx.req.param('id'); + const url = `https://www.ganjingworld.com/channel/${id}?tab=articles`; + const apiUrl = `https://gw.ganjingworld.com/v1.1/content/get-by-channel?channel_id=${id}&content_type=News`; + // const apiUrl = `https://gw.ganjingworld.com/v1.1/content/get-by-channel?channel_id=1fcahpcut9t3gz4zIvYSJR7qd1cs0c&content_type=News`; + + const parsed: ApiResponse = await ofetch(apiUrl); + if (parsed.data.list.length === 0) { + throw new Error('No articles found for this channel. Please make sure the channel ID is correct and that the channel contains articles.'); + } + const title = parsed.data.list[0].channel.name; + const items = await Promise.all( + parsed.data.list.map((item) => + cache.tryGet(item.id, async () => { + const pubDate = new Date(item.time_scheduled); + const fetchArticleUrl = `https://gw.ganjingworld.com/v1.1/content/query?lang=zh-TW&query=basic%2Cfull%2Ctranslations%2Clike%2Cshare%2Csave%2Cview%2Ctag_list&ids=${item.id}`; + const parsedArticle: ApiResponse = await ofetch(fetchArticleUrl); + + const description = parsedArticle.data.list[0]?.text ?? ''; + + return { + title: item.title, + link: `https://www.ganjingworld.com/news/${item.id}`, + pubDate, + description, + }; + }) + ) + ); + + return { + title, + link: url, + description: `Articles from Ganjing World Channel ${title}`, + item: items, + }; +} diff --git a/lib/routes/ganjingworld/channel/posts.ts b/lib/routes/ganjingworld/channel/posts.ts new file mode 100644 index 000000000..83d4522a8 --- /dev/null +++ b/lib/routes/ganjingworld/channel/posts.ts @@ -0,0 +1,66 @@ +// RSSHub route for fetching posts from Ganjing World. +// Returns a list of posts in a channel. +// Source: https://www.ganjingworld.com + +import type { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; + +import type { ApiResponse } from '../interfaces/api'; + +export const route: Route = { + path: '/channel/posts/:id', + categories: ['social-media'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + example: '/ganjingworld/channel/posts/1fcahpcut9t3gz4zIvYSJR7qd1cs0c', + parameters: { id: 'Channel ID, can be found in channel url' }, + radar: [ + { + source: ['ganjingworld.com'], + target: '/channel/posts/:id', + }, + ], + url: 'www.ganjingworld.com', + name: 'posts in a channel', + maintainers: ['yixiangli2001'], + + handler, +}; + +async function handler(ctx) { + const id = ctx.req.param('id'); + const url = `https://www.ganjingworld.com/channel/${id}?tab=posts`; + const apiUrl = `https://gw.ganjingworld.com/v1.0c/social-content/get-owner-posts?owner_id=${id}&start_key=&page_size=48&post_image=true&query=basic,post,owner,comment,view,like,is_liked,share `; + // const apiUrl = `https://gw.ganjingworld.com/v1.0c/social-content/get-owner-posts?owner_id=1fcahpcut9t3gz4zIvYSJR7qd1cs0c&start_key=&page_size=48&post_image=true&query=basic,post,owner,comment,view,like,is_liked,share%20Request%20Method `; + + const parsed: ApiResponse = await ofetch(apiUrl); + if (parsed.data.list.length === 0) { + throw new Error('No posts found for this channel. Please make sure the channel ID is correct and that the channel contains posts.'); + } + const title = parsed.data.list[0].channel.name; + const items = parsed.data.list.map((item) => { + const pubDate = new Date(item.time_scheduled); + + const raw = item.text?.replaceAll('\n', '
') || ''; + const textWithMedia = item.media?.length > 0 ? raw + `
` : raw; + return { + title: item.title, + link: `https://www.ganjingworld.com/news/${item.id}`, + pubDate, + description: textWithMedia, + }; + }); + + return { + title, + link: url, + description: `Posts from Ganjing World Channel ${title}`, + item: items, + }; +} diff --git a/lib/routes/ganjingworld/channel/shorts.ts b/lib/routes/ganjingworld/channel/shorts.ts new file mode 100644 index 000000000..e8d49b4e8 --- /dev/null +++ b/lib/routes/ganjingworld/channel/shorts.ts @@ -0,0 +1,67 @@ +// RSSHub route for fetching shorts from Ganjing World. +// Returns a list of shorts in a channel. +// Source: https://www.ganjingworld.com +import type { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; + +import type { ApiResponse } from '../interfaces/api'; + +export const route: Route = { + path: '/channel/shorts/:id', + categories: ['social-media'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + example: '/ganjingworld/channel/shorts/1fq5chh3ajo67UNu14uAvfzOp1a80c', + parameters: { id: 'Channel ID, can be found in channel url' }, + radar: [ + { + source: ['ganjingworld.com'], + target: '/channel/shorts/:id', + }, + ], + url: 'www.ganjingworld.com', + name: 'Shorts in a channel', + maintainers: ['yixiangli2001'], + handler, +}; + +async function handler(ctx) { + const id = ctx.req.param('id'); + const url = `https://www.ganjingworld.com/channel/${id}?tab=shorts`; + const apiUrl = `https://gw.ganjingworld.com/v1.1/content/get-by-channel?channel_id=${id}&content_type=Shorts&page_size=30&query=basic%2Cwatch_count&visibility=public&mode=ready`; + + const parsed: ApiResponse = await ofetch(apiUrl); + if (parsed.data.list.length === 0) { + throw new Error('No shorts found for this channel. Please make sure the channel ID is correct and that the channel contains shorts.'); + } + const title = parsed.data.list[0].channel.name; + const items = parsed.data.list.map((item) => { + const pubDate = new Date(item.time_scheduled); + const videoUrl = item.video_url || (item.media.length > 0 ? item.media[0].url : ''); + const posterUrl = item.poster_url || item.image_auto_url || ''; + + let description = item.description || ''; + if (videoUrl) { + description = `
${description}`; + } + + return { + title: item.title, + link: `https://www.ganjingworld.com/video/${item.id}`, + pubDate, + description, + }; + }); + return { + title, + link: url, + description: `Short videos from Channel ${title}`, + item: items, + }; +} diff --git a/lib/routes/ganjingworld/channel/videos.ts b/lib/routes/ganjingworld/channel/videos.ts new file mode 100644 index 000000000..76479c73b --- /dev/null +++ b/lib/routes/ganjingworld/channel/videos.ts @@ -0,0 +1,68 @@ +// RSSHub route for fetching videos from Ganjing World. +// Returns a list of videos in a channel. +// Source: https://www.ganjingworld.com + +import type { Route } from '@/types'; +import ofetch from '@/utils/ofetch'; + +import type { ApiResponse } from '../interfaces/api'; + +export const route: Route = { + path: '/channel/videos/:id', + categories: ['social-media'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + example: '/ganjingworld/channel/videos/1eiqjdnq7go1OPYtIbLDVMpM61ok0c', + parameters: { id: 'Channel ID, can be found in channel url' }, + radar: [ + { + source: ['ganjingworld.com'], + target: '/channel/videos/:id', + }, + ], + url: 'www.ganjingworld.com', + name: 'Videos in a channel', + maintainers: ['yixiangli2001'], + + handler, +}; + +async function handler(ctx) { + const id = ctx.req.param('id'); + const url = `https://www.ganjingworld.com/channel/${id}`; + const apiUrl = `https://gw.ganjingworld.com/v1.1/content/get-by-channel?lang=zh-TW&start_key=&channel_id=${id}&page_size=24&content_type=AllVideo&query=basic%2Ctranslations%2Cwatch_count&visibility=public&mode=ready`; + // const apiUrl = `https://gw.ganjingworld.com/v1.1/content/get-by-channel?lang=zh-TW&start_key=&channel_id=1eiqjdnq7go1OPYtIbLDVMpM61ok0c&page_size=24&content_type=AllVideo&query=basic%2Ctranslations%2Cwatch_count&visibility=public&mode=ready`; + + const parsed: ApiResponse = await ofetch(apiUrl); + const title = parsed.data.list.length > 0 ? parsed.data.list[0].channel.name : 'Ganjing World Channel'; + const items = parsed.data.list.map((item) => { + const pubDate = new Date(item.time_scheduled); + const videoUrl = item.video_url || (item.media.length > 0 ? item.media[0].url : ''); + const posterUrl = item.poster_url || item.image_auto_url || ''; + + let description = item.description || ''; + if (videoUrl) { + description = `
${description}`; + } + + return { + title: item.title, + link: `https://www.ganjingworld.com/video/${item.id}`, + pubDate, + description, + }; + }); + + return { + title, + link: url, + description: `Videos from Ganjing World Channel ${title}`, + item: items, + }; +} diff --git a/lib/routes/ganjingworld/interfaces/api.ts b/lib/routes/ganjingworld/interfaces/api.ts new file mode 100644 index 000000000..c41931f65 --- /dev/null +++ b/lib/routes/ganjingworld/interfaces/api.ts @@ -0,0 +1,34 @@ +// interfaces/api.ts +export interface ApiResponse { + data: { + list: ListItem[]; + }; +} + +export interface ListItem { + title: string; + description?: string; + id: string; + time_scheduled: number; + channel: Channel; + poster_url: string; + image_auto_url?: string; + video_url: string; + text?: string; + media: Media[]; + watch_count?: number; +} +export interface Channel { + icon: string; + id: string; + name: string; +} + +export interface Media { + type: string; + id: string; + url: string; + width: number; + height: number; + ratio: number; +} diff --git a/lib/routes/ganjingworld/namespace.ts b/lib/routes/ganjingworld/namespace.ts new file mode 100644 index 000000000..97c69342f --- /dev/null +++ b/lib/routes/ganjingworld/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Ganjing World', + url: 'www.ganjingworld.com/', + description: + '"Gan Jing" means Clean. Through the utilization of technology, Gan Jing World is dedicated to revitalizing traditional connections—fostering a culture of care, kindness, mutual respect, and trust among individuals, within families, and throughout society.', + lang: 'en', +};