diff --git a/lib/v2/otobanana/cast.js b/lib/v2/otobanana/cast.js
new file mode 100644
index 000000000..bb45141a3
--- /dev/null
+++ b/lib/v2/otobanana/cast.js
@@ -0,0 +1,29 @@
+const got = require('@/utils/got');
+const { apiBase, baseUrl, getUserInfo, renderCast } = require('./utils');
+
+module.exports = async (ctx) => {
+ const { id } = ctx.params;
+
+ const userInfo = await getUserInfo(id, ctx.cache.tryGet);
+ const { data: castData } = await got(`${apiBase}/users/${id}/casts/`);
+
+ const casts = castData.results.map((item) => renderCast(item));
+
+ ctx.state.data = {
+ title: `${userInfo.name} (@${userInfo.username}) - 音声投稿 | OTOBANANA`,
+ description: userInfo.bio.replace(/\n/g, ' '),
+ link: `${baseUrl}/user/${id}`,
+ image: userInfo.avatar_url,
+ icon: userInfo.avatar_url,
+ logo: userInfo.avatar_url,
+ language: 'ja',
+ author: userInfo.name,
+ itunes_author: userInfo.name,
+ item: casts,
+ };
+
+ ctx.state.json = {
+ userInfo,
+ castData,
+ };
+};
diff --git a/lib/v2/otobanana/livestream.js b/lib/v2/otobanana/livestream.js
new file mode 100644
index 000000000..e8f029e48
--- /dev/null
+++ b/lib/v2/otobanana/livestream.js
@@ -0,0 +1,29 @@
+const got = require('@/utils/got');
+const { apiBase, baseUrl, getUserInfo, renderLive } = require('./utils');
+
+module.exports = async (ctx) => {
+ const { id } = ctx.params;
+
+ const userInfo = await getUserInfo(id, ctx.cache.tryGet);
+ const { data: liveData } = await got(`${apiBase}/users/${id}/livestreams/`);
+
+ const casts = liveData.results.map((item) => renderLive(item));
+
+ ctx.state.data = {
+ title: `${userInfo.name} (@${userInfo.username}) - ライブ配信 | OTOBANANA`,
+ description: userInfo.bio.replace(/\n/g, ' '),
+ link: `${baseUrl}/user/${id}`,
+ image: userInfo.avatar_url,
+ icon: userInfo.avatar_url,
+ logo: userInfo.avatar_url,
+ language: 'ja',
+ author: userInfo.name,
+ itunes_author: userInfo.name,
+ item: casts,
+ };
+
+ ctx.state.json = {
+ userInfo,
+ liveData,
+ };
+};
diff --git a/lib/v2/otobanana/maintainer.js b/lib/v2/otobanana/maintainer.js
new file mode 100644
index 000000000..1db1a6d2f
--- /dev/null
+++ b/lib/v2/otobanana/maintainer.js
@@ -0,0 +1,5 @@
+module.exports = {
+ '/user/:id': ['TonyRL'],
+ '/user/:id/cast': ['TonyRL'],
+ '/user/:id/livestream': ['TonyRL'],
+};
diff --git a/lib/v2/otobanana/radar.js b/lib/v2/otobanana/radar.js
new file mode 100644
index 000000000..3927bb424
--- /dev/null
+++ b/lib/v2/otobanana/radar.js
@@ -0,0 +1,25 @@
+module.exports = {
+ 'otobanana.com': {
+ _name: 'OTOBANANA',
+ '.': [
+ {
+ title: 'Timeline タイムライン',
+ docs: 'https://docs.rsshub.app/multimedia#otobanana',
+ source: ['/user/:id'],
+ target: '/otobanana/user/:id',
+ },
+ {
+ title: 'Cast 音声投稿',
+ docs: 'https://docs.rsshub.app/multimedia#otobanana',
+ source: ['/user/:id/cast', '/user/:id'],
+ target: '/otobanana/user/:id/cast',
+ },
+ {
+ title: 'Livestream ライブ配信',
+ docs: 'https://docs.rsshub.app/multimedia#otobanana',
+ source: ['/user/:id/livestream', '/user/:id'],
+ target: '/otobanana/user/:id/livestream',
+ },
+ ],
+ },
+};
diff --git a/lib/v2/otobanana/router.js b/lib/v2/otobanana/router.js
new file mode 100644
index 000000000..1d3ec6d66
--- /dev/null
+++ b/lib/v2/otobanana/router.js
@@ -0,0 +1,5 @@
+module.exports = (router) => {
+ router.get('/user/:id', require('./timeline'));
+ router.get('/user/:id/cast', require('./cast'));
+ router.get('/user/:id/livestream', require('./livestream'));
+};
diff --git a/lib/v2/otobanana/templates/description.art b/lib/v2/otobanana/templates/description.art
new file mode 100644
index 000000000..72a6921cf
--- /dev/null
+++ b/lib/v2/otobanana/templates/description.art
@@ -0,0 +1,11 @@
+{{ if cast }}
+
+
+
+
+💬 {{ cast.comment_count }} ❤️ {{ cast.like_count }} 🍌 {{ cast.gift_banana }} {{ cast.play_count }} 再生
+
+{{@ cast.text.replace(/\n/g, '
') }}
+{{ /if }}
diff --git a/lib/v2/otobanana/timeline.js b/lib/v2/otobanana/timeline.js
new file mode 100644
index 000000000..93984b608
--- /dev/null
+++ b/lib/v2/otobanana/timeline.js
@@ -0,0 +1,29 @@
+const got = require('@/utils/got');
+const { apiBase, baseUrl, getUserInfo, renderPost } = require('./utils');
+
+module.exports = async (ctx) => {
+ const { id } = ctx.params;
+
+ const userInfo = await getUserInfo(id, ctx.cache.tryGet);
+ const { data: postData } = await got(`${apiBase}/users/${id}/posts/`);
+
+ const posts = postData.results.map((item) => renderPost(item));
+
+ ctx.state.data = {
+ title: `${userInfo.name} (@${userInfo.username}) - タイムライン | OTOBANANA`,
+ description: userInfo.bio.replace(/\n/g, ' '),
+ link: `${baseUrl}/user/${id}`,
+ image: userInfo.avatar_url,
+ icon: userInfo.avatar_url,
+ logo: userInfo.avatar_url,
+ language: 'ja',
+ author: userInfo.name,
+ itunes_author: userInfo.name,
+ item: posts,
+ };
+
+ ctx.state.json = {
+ userInfo,
+ postData,
+ };
+};
diff --git a/lib/v2/otobanana/utils.js b/lib/v2/otobanana/utils.js
new file mode 100644
index 000000000..61b4d853c
--- /dev/null
+++ b/lib/v2/otobanana/utils.js
@@ -0,0 +1,67 @@
+const got = require('@/utils/got');
+const { parseDate } = require('@/utils/parse-date');
+const { art } = require('@/utils/render');
+const { join } = require('path');
+
+const domain = 'otobanana.com';
+const apiBase = `https://api.${domain}`;
+const baseUrl = `https://${domain}`;
+
+const getUserInfo = (id, tryGet) =>
+ tryGet(`otobanana:user:${id}`, async () => {
+ const { data } = await got(`${apiBase}/users/${id}/`);
+ return data;
+ });
+
+const renderCast = (cast) => ({
+ title: cast.title,
+ description: art(join(__dirname, 'templates/description.art'), { cast }),
+ pubDate: parseDate(cast.created_at),
+ link: `https://otobanana.com/cast/${cast.id}`,
+ author: `${cast.user.name} (@${cast.user.username})`,
+ itunes_item_image: cast.thumbnail_url,
+ itunes_duration: cast.duration_time,
+ enclosure_url: cast.audio_url,
+ enclosure_type: 'audio/x-m4a',
+ upvotes: cast.like_count,
+ comments: cast.comment_count,
+});
+
+const renderLive = (live) => ({
+ title: live.title,
+ description: live.is_open ? '配信中のライブ' : '終了しました',
+ pubDate: parseDate(live.created_at),
+ link: live.room_url,
+ guid: `${live.room_url}#${live.id}`,
+ author: `${live.user.name} (@${live.user.username})`,
+ upvotes: live.like_count,
+ comments: live.comment_count,
+});
+
+const renderPost = ({ id, type_label: type, cast, /** livestream */ message /** , event */ }) => {
+ switch (type) {
+ case 'cast':
+ return renderCast(cast);
+ case 'message':
+ return {
+ title: message.text.split('\n')[0],
+ description: message.text.replace(/\n/g, '
'),
+ pubDate: parseDate(message.created_at),
+ link: `https://otobanana.com/${type}/${id}`,
+ author: `${message.user.name} (@${message.user.username})`,
+ upvotes: message.like_count,
+ comments: message.comment_count,
+ };
+ default:
+ throw Error(`Unknown post type: ${type}`);
+ }
+};
+
+module.exports = {
+ apiBase,
+ baseUrl,
+ getUserInfo,
+ renderCast,
+ renderLive,
+ renderPost,
+};
diff --git a/website/docs/routes/multimedia.mdx b/website/docs/routes/multimedia.mdx
index f6f5b4f34..cd2e33066 100644
--- a/website/docs/routes/multimedia.mdx
+++ b/website/docs/routes/multimedia.mdx
@@ -957,6 +957,20 @@ JavDB 有多个备用域名,本路由默认使用永久域名 `https://javdb.c
`day` 类型的关键词必须填写 **日期** ,按照示例写成形如 `20200730` 的格式
+## OTOBANANA {#otobanana}
+
+### Timeline タイムライン {#otobanana-timeline-%E3%82%BF%E3%82%A4%E3%83%A0%E3%83%A9%E3%82%A4%E3%83%B3}
+
+
+
+### Cast 音声投稿 {#otobanana-cast-yin-sheng-tou-gao}
+
+
+
+### Livestream ライブ配信 {#otobanana-livestream-%E3%83%A9%E3%82%A4%E3%83%96-pei-xin}
+
+
+
## PornHub {#pornhub}
### Category {#pornhub-category}