diff --git a/docs/en/social-media.md b/docs/en/social-media.md
index 4e477c3d45..2f30b32d8a 100644
--- a/docs/en/social-media.md
+++ b/docs/en/social-media.md
@@ -336,6 +336,27 @@ This route requires Twitter token's corresponding id, therefore it's only availa
+## Vimeo
+
+### User Profile
+
+
+::: tip Special category name attention
+
+Some of the categories contain slash like `3D/CG` , must change the slash `/` to the vertical bar`|`.
+
+:::
+
+
+
+### Channel
+
+
+
+### Category
+
+
+
## YouTube
::: tip Tiny Tiny RSS users please notice
diff --git a/docs/social-media.md b/docs/social-media.md
index 3cb2b140e1..82a13f2bd6 100644
--- a/docs/social-media.md
+++ b/docs/social-media.md
@@ -695,6 +695,32 @@ Tiny Tiny RSS 会给所有 iframe 元素添加 `sandbox="allow-scripts"` 属性
+## Vimeo
+
+### 用户页面
+
+
+
+::: tip 请注意带有斜杠的的标签名
+
+如果分类名带有斜杠符号的如 `3D/CG` 时,必须把斜杠`/`转成 `|`
+
+:::
+
+
+
+### 频道页面
+
+
+
+
+
+### 分类页面
+
+
+
+
+
## VueVlog
### 用户
diff --git a/lib/v2/vimeo/category.js b/lib/v2/vimeo/category.js
new file mode 100644
index 0000000000..ffffd52227
--- /dev/null
+++ b/lib/v2/vimeo/category.js
@@ -0,0 +1,64 @@
+const got = require('@/utils/got');
+const cheerio = require('cheerio');
+const { parseDate } = require('@/utils/parse-date');
+const { art } = require('@/utils/render');
+const path = require('path');
+
+module.exports = async (ctx) => {
+ const { category, staffpicks } = ctx.params;
+
+ const categoryparams = category;
+ let sortparams = '&direction=desc&sort=date';
+ let feedtitle = category;
+ if (staffpicks && staffpicks !== 'staffpicks') {
+ return;
+ }
+
+ if (staffpicks) {
+ sortparams += '&filter=conditional_featured';
+ feedtitle += `: ${category} staffpicks`;
+ }
+
+ const url = `https://api.vimeo.com/categories/${categoryparams}/videos?page=1&per_page=18${sortparams}`;
+ const tokenresponse = await got({
+ method: 'get',
+ url: 'https://vimeo.com/_rv/viewer',
+ });
+ const VimeoAuthorization = tokenresponse.data.jwt;
+
+ const response = await got({
+ method: 'get',
+ url,
+ headers: {
+ Authorization: `jwt ${VimeoAuthorization}`,
+ },
+ });
+
+ const vimeojs = response.data.data;
+
+ const feedlink = `https://vimeo.com/categories/${category}/videos/sort:latest`;
+ const feedlinkstaffpicks = '?staffpicked=ture';
+ const feedDescription = await ctx.cache.tryGet(feedlink + (staffpicks ? feedlinkstaffpicks : ''), async () => {
+ const response = await got({
+ url: feedlink + (staffpicks ? feedlinkstaffpicks : ''),
+ });
+ const description = cheerio.load(response.data);
+ return description('meta[name="description"]').attr('content');
+ });
+
+ ctx.state.data = {
+ title: `${feedtitle} | Vimeo category`,
+ link: feedlink + (staffpicks ? feedlinkstaffpicks : ''),
+ description: feedDescription,
+ item: vimeojs.map((item) => ({
+ title: item.name,
+ description: art(path.join(__dirname, 'templates/description.art'), {
+ videoUrl: item.uri.replace(`/videos`, ''),
+ vdescription: item.description ? item.description : '',
+ }),
+ pubDate: parseDate(item.created_time),
+ link: item.link,
+ author: item.user.name,
+ })),
+ };
+};
diff --git a/lib/v2/vimeo/channel.js b/lib/v2/vimeo/channel.js
new file mode 100644
index 0000000000..9b0a764252
--- /dev/null
+++ b/lib/v2/vimeo/channel.js
@@ -0,0 +1,72 @@
+const got = require('@/utils/got');
+const cheerio = require('cheerio');
+const { parseDate } = require('@/utils/parse-date');
+const { art } = require('@/utils/render');
+const path = require('path');
+
+module.exports = async (ctx) => {
+ const channel = ctx.params.channel;
+ const url = `https://vimeo.com/channels/${channel}/videos`;
+ const page1 = await got({
+ method: 'get',
+ url: `${url}/page:1/sort:date/format:detail`,
+ headers: {
+ 'X-Requested-With': 'XMLHttpRequest',
+ },
+ });
+ const page2 =
+ channel === `bestoftheyear`
+ ? await got({
+ method: 'get',
+ url: `${url}/page:2/sort:date/format:detail`,
+ headers: {
+ 'X-Requested-With': 'XMLHttpRequest',
+ },
+ })
+ : '';
+ const pagedata = page1.data.concat(page2.data);
+ const $ = cheerio.load(pagedata);
+ const list = $('ol li.clearfix');
+
+ const description = await Promise.all(
+ list.get().map((item) => {
+ item = $(item);
+ const link = item.find('.more').attr('href');
+ return ctx.cache.tryGet(link, async () => {
+ const response2 = await got({
+ method: 'get',
+ url: `https://vimeo.com${link}/description?breeze=1`,
+ headers: {
+ 'X-Requested-With': 'XMLHttpRequest',
+ 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) ',
+ },
+ });
+ const articledata = response2.data;
+ const $2 = cheerio.load(articledata);
+ $2('span').remove();
+ return $2.html();
+ });
+ })
+ );
+ ctx.state.data = {
+ title: `${channel} | Vimeo channel`,
+ link: url,
+ item: list
+ .map((index, item) => {
+ item = $(item);
+ const title = item.find('.title a').text();
+ const author = item.find('.meta a').text();
+ return {
+ title,
+ description: art(path.join(__dirname, 'templates/description.art'), {
+ videoUrl: item.find('.more').attr('href'),
+ vdescription: description[index] ? description[index] : '',
+ }),
+ pubDate: parseDate(item.find('time').attr('datetime')),
+ link: `https://vimeo.com${item.find('.more').attr('href')}`,
+ author,
+ };
+ })
+ .get(),
+ };
+};
diff --git a/lib/v2/vimeo/radar.js b/lib/v2/vimeo/radar.js
new file mode 100644
index 0000000000..1edb5b7925
--- /dev/null
+++ b/lib/v2/vimeo/radar.js
@@ -0,0 +1,33 @@
+module.exports = {
+ 'vimeo.com': {
+ _name: 'Vimeo',
+ '.': [
+ {
+ title: 'User videos',
+ docs: 'https://docs.rsshub.app/social-media.html#vimeo-yong-hu-ye-mian',
+ source: '/:username/',
+ target: (params, url, document) => {
+ const uid = document && document.querySelector('html').innerHTML.match(/app.vimeo.com\/users\/([0-9]+)/)[1];
+ return uid ? `/vimeo/user/${uid}` : '';
+ },
+ },
+ {
+ title: 'User Video Category',
+ docs: 'https://docs.rsshub.app/social-media.html#vimeo-yong-hu-ye-mian',
+ source: '/',
+ },
+ {
+ title: 'Channel',
+ docs: 'https://docs.rsshub.app/social-media.html#vimeo-channel',
+ source: ['/channels/:channel', '/channels/:channel/videos', '/channels/:channel/videos/:sort/:format'],
+ target: '/vimeo/channel/:channel',
+ },
+ {
+ title: 'Category',
+ docs: 'https://docs.rsshub.app/social-media.html#vimeo-category',
+ source: ['/categories/:category', '/categories/:category/:subcategory', '/categories/:category/:subcategory/videos'],
+ target: (params) => `/vimeo/category/:category${params.subcategory ? `/` + params.subcategory : ''}`,
+ },
+ ],
+ },
+};
diff --git a/lib/v2/vimeo/router.js b/lib/v2/vimeo/router.js
new file mode 100644
index 0000000000..d1ee9a681c
--- /dev/null
+++ b/lib/v2/vimeo/router.js
@@ -0,0 +1,5 @@
+module.exports = (router) => {
+ router.get('/category/:category/:staffpicks?', require('./category'));
+ router.get('/channel/:channel', require('./channel'));
+ router.get('/user/:username/:cat?', require('./usr-videos'));
+};
diff --git a/lib/v2/vimeo/templates/description.art b/lib/v2/vimeo/templates/description.art
new file mode 100644
index 0000000000..2ea8f44f7e
--- /dev/null
+++ b/lib/v2/vimeo/templates/description.art
@@ -0,0 +1,4 @@
+
+{{ if vdescription }}
+
{{@ vdescription }}
+{{ /if }}
diff --git a/lib/v2/vimeo/usr-videos.js b/lib/v2/vimeo/usr-videos.js
new file mode 100644
index 0000000000..c97511f60a
--- /dev/null
+++ b/lib/v2/vimeo/usr-videos.js
@@ -0,0 +1,76 @@
+const got = require('@/utils/got');
+const { parseDate } = require('@/utils/parse-date');
+const { art } = require('@/utils/render');
+const path = require('path');
+
+module.exports = async (ctx) => {
+ const tokenresponse = await got({
+ method: 'get',
+ url: 'https://vimeo.com/_rv/viewer',
+ });
+ const VimeoAuthorization = tokenresponse.data.jwt;
+
+ const { username, cat } = ctx.params;
+
+ const profileresponse = await got({
+ method: 'get',
+ url: `https://api.vimeo.com/users/${username}?fields=name,gender,bio,uri,link,categories&fetch_user_profile=1`,
+ headers: {
+ Authorization: `jwt ${VimeoAuthorization}`,
+ },
+ });
+
+ const profilesjs = profileresponse.data;
+ const catjs = profileresponse.data.categories;
+
+ let catword;
+ if (cat && catjs.length > 0) {
+ for (let s = 0; s < catjs.length; s++) {
+ if (decodeURIComponent(cat).replace('|', '/') === catjs[s].name) {
+ catword = catjs[s].word;
+ }
+ }
+ }
+
+ let urlfilter = `&filter=category&filter_category=${catword}`;
+ if (!cat) {
+ urlfilter = '';
+ }
+ if (!catword && cat && cat !== 'picks') {
+ return '';
+ }
+ const picked = cat && cat === 'picks';
+
+ const Vimeocat = `${profilesjs.uri}/videos?fields=name,uri,description,created_time&include_videos=1&page=1&per_page=10${urlfilter}`;
+ const Vimeoallpicks = `/users/${username}/profile_sections?fields=videos.data.clip.name,videos.data.clip.uri,videos.data.clip.description,videos.data.clip.created_time&include_videos=1&badge=1&page=1&per_page=10`;
+
+ const contentresponse = await got({
+ method: 'get',
+ url: `https://api.vimeo.com${picked ? Vimeoallpicks : Vimeocat}`,
+ headers: {
+ Authorization: `jwt ${VimeoAuthorization}`,
+ },
+ });
+ const vimeojs = picked ? contentresponse.data.data[0].videos.data : contentresponse.data.data;
+
+ ctx.state.data = {
+ title: `${profilesjs.name} ${catword ? cat.replace('|', '/') : ''} ${picked ? 'picks' : ''} | Vimeo `,
+ link: profilesjs.link,
+ description: profilesjs.bio,
+
+ item: vimeojs.map((item) => {
+ const vdescription = picked ? item.clip.description : item.description;
+
+ return {
+ title: picked ? item.clip.name : item.name,
+ description: art(path.join(__dirname, 'templates/description.art'), {
+ videoUrl: picked ? item.clip.uri.replace('/videos', '') : item.uri.replace('/videos', ''),
+ vdescription: vdescription ? vdescription.replace(/\n/g, '
') : '',
+ }),
+ pubDate: parseDate(picked || cat ? '' : item.created_time),
+ link: `https://vimeo.com${picked ? item.clip.uri.replace('videos/', '') : item.uri.replace('videos/', '')}`,
+ author: profilesjs.name,
+ };
+ }),
+ };
+};