feat(route): Add GanjingWorld (#20086)

* Created route for channel videos

* chore: updated route name

* feat: add short video route

* Added Article Route

* Added Posts Route

* chore: change file name

* chore: reorder folder structure

* Update lib/routes/ganjingworld/namespace.ts

Co-authored-by: Tony <TonyRL@users.noreply.github.com>

* fix(route/ganjingworld): fix target url; fix repeated name, fix redundant parsing, fix video poster attribute, fix naming convention

* fix(route/ganjingworld) fix examples

* fix(route/ganjingworld) fix optional empty string

* chore(route/ganjingworld): fix example link

* style: auto format

* New files from Fly.io Launch

* Revert "New files from Fly.io Launch"

This reverts commit 71d9f42f5f0073f35bc34ac4e9417e4a4a1d94fc.

* Fix: resolved requested changes

* fix: implement suggested changes

* fix: fix variable names

* fix: update error messages and improve data handling in Ganjing World routes

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Fly.io <noreply@fly.io>
Co-authored-by: fly-io[bot] <52462049+fly-io[bot]@users.noreply.github.com>
This commit is contained in:
Shirley Yi 2026-01-20 04:14:22 +11:00 committed by GitHub
parent 6d8e143916
commit dd5e1ffcf8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 317 additions and 0 deletions

View File

@ -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<ApiResponse>(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<ApiResponse>(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,
};
}

View File

@ -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<ApiResponse>(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', '<br>') || '';
const textWithMedia = item.media?.length > 0 ? raw + `<figure><img src="${item.media[0].url}"></figure>` : 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,
};
}

View File

@ -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<ApiResponse>(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 = `<video controls src="${videoUrl}" poster="${posterUrl}" style="max-width: 100%;"></video><br/>${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,
};
}

View File

@ -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<ApiResponse>(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 = `<video controls src="${videoUrl}" poster="${posterUrl}" style="max-width: 100%;"></video><br/>${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,
};
}

View File

@ -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;
}

View File

@ -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',
};