mirror of
https://github.com/DIYgod/RSSHub.git
synced 2026-08-31 01:01:11 +08:00
feat(route): add spotify (#8966)
* feat: /spotify/playlist/:id
* feat: /spotify/artist
* feat(spotify/artist): add to radar
* feat: /spotify/saved
* feat: /spotify/top/{tracks,artists}
* feat(spotify): add images for artist and playlist
* docs: /spotify/*
* docs/en: /spotify configurations
* chore(spotify): apiKey/Secret -> clientId/Secret
* fix(spotify/utils): genres can be empty
This commit is contained in:
@@ -20,6 +20,15 @@
|
||||
"DISQUS_API_KEY": {
|
||||
"required": false
|
||||
},
|
||||
"SPOTIFY_CLIENT_ID": {
|
||||
"required": false
|
||||
},
|
||||
"SPOTIFY_CLIENT_SECRET": {
|
||||
"required": false
|
||||
},
|
||||
"SPOTIFY_REFRESHTOKEN": {
|
||||
"required": false
|
||||
},
|
||||
"TWITTER_CONSUMER_KEY": {
|
||||
"required": false
|
||||
},
|
||||
|
||||
@@ -538,6 +538,16 @@ See docs of the specified route and `lib/config.js` for detailed information.
|
||||
|
||||
- `GITHUB_ACCESS_TOKEN`: GitHub Access Token
|
||||
|
||||
- spotify: [API key registration](https://developer.spotify.com)
|
||||
|
||||
- `SPOTIFY_CLIENT_ID`:Client ID of the application
|
||||
|
||||
- `SPOTIFY_CLIENT_SECRET`:Client secret of the application
|
||||
|
||||
- spotify (user data related routes):
|
||||
|
||||
- `SPOTIFY_REFRESHTOKEN`:The refresh token of the user f the Spotify application. Check [this gist](https://gist.github.com/outloudvi/d1bbeb5e989db5385384a223a7263744) for detailed information.
|
||||
|
||||
- Instagram:
|
||||
|
||||
- `IG_USERNAME`: Your Instagram username
|
||||
|
||||
@@ -150,6 +150,30 @@ Refer to [Pornhub F.A.Qs](https://help.pornhub.com/hc/en-us/articles/36004432703
|
||||
|
||||
<RouteEn author="fallenhh" example="/soundcloud/tracks/angeart" path="/soundcloud/tracks/:user" :paramsDesc="['User name']" />
|
||||
|
||||
## Spotify
|
||||
|
||||
|
||||
### Artist Albums
|
||||
|
||||
<RouteEn author="outloudvi" example="/spotify/artist/6k9TBCxyr4bXwZ8Y21Kwn1" path="/spotify/artist/:id" :paramsDesc="['Artist ID']" />
|
||||
|
||||
### Playlist
|
||||
|
||||
<RouteEn author="outloudvi" example="/spotify/playlist/4UBVy1LttvodwivPUuwJk2" path="/spotify/playlist/:id" :paramsDesc="['Playlist ID']" />
|
||||
|
||||
### Personal Saved Tracks
|
||||
|
||||
<RouteEn author="outloudvi" example="/spotify/saved/50" path="/spotify/saved/:limit?" :paramsDesc="['Track count, 50 by default']" />
|
||||
|
||||
### Personal Top Tracks
|
||||
|
||||
<RouteEn author="outloudvi" example="/spotify/top/tracks" path="/spotify/top/tracks" />
|
||||
|
||||
### Personal Top Artists
|
||||
|
||||
<RouteEn author="outloudvi" example="/spotify/top/artists" path="/spotify/top/artists" />
|
||||
|
||||
|
||||
## Trakt.tv
|
||||
|
||||
### User Collection
|
||||
|
||||
@@ -548,6 +548,16 @@ RSSHub 支持使用访问密钥 / 码,白名单和黑名单三种方式进行
|
||||
3. 点击 dynamic_new 请求,找到 Cookie
|
||||
4. 视频和专栏只要求 `SESSDATA` 字段,动态需复制整段 Cookie
|
||||
|
||||
- spotify 全部路由: [注册地址](https://developer.spotify.com)
|
||||
|
||||
- `SPOTIFY_CLIENT_ID`:Spotify 应用的 client ID
|
||||
|
||||
- `SPOTIFY_CLIENT_SECRET`:Spotify 应用的 client secret
|
||||
|
||||
- spotify 用户相关路由
|
||||
|
||||
- `SPOTIFY_REFRESHTOKEN`:用户在此 Spotify 应用的 refresh token。可以利用 [此 gist](https://gist.github.com/outloudvi/d1bbeb5e989db5385384a223a7263744) 获取。
|
||||
|
||||
- 语雀 全部路由:[注册地址](https://www.yuque.com/register)
|
||||
|
||||
- `YUQUE_TOKEN`: 语雀 Token,[获取地址](https://www.yuque.com/settings/tokens)。语雀接口做了访问频率限制,为保证正常访问建议配置 Token,详见 [语雀开发者文档](https://www.yuque.com/yuque/developer/api#5b3a1535)。
|
||||
|
||||
@@ -826,6 +826,28 @@ JavDB 有多个备用域名,本路由以 <https://javdb7.com> 为默认域名
|
||||
|
||||
<Route author="fallenhh" example="/soundcloud/tracks/angeart" path="/soundcloud/tracks/:user" :paramsDesc="['用户名']" />
|
||||
|
||||
## Spotify
|
||||
|
||||
### 艺术家专辑
|
||||
|
||||
<Route author="outloudvi" example="/spotify/artist/6k9TBCxyr4bXwZ8Y21Kwn1" path="/spotify/artist/:id" :paramsDesc="['艺术家 ID']" />
|
||||
|
||||
### 播放列表
|
||||
|
||||
<Route author="outloudvi" example="/spotify/playlist/4UBVy1LttvodwivPUuwJk2" path="/spotify/playlist/:id" :paramsDesc="['播放列表 ID']" />
|
||||
|
||||
### 个人 Saved Tracks
|
||||
|
||||
<Route author="outloudvi" example="/spotify/saved/50" path="/spotify/saved/:limit?" :paramsDesc="['歌曲数量,默认为 50']" />
|
||||
|
||||
### 个人 Top Tracks
|
||||
|
||||
<Route author="outloudvi" example="/spotify/top/tracks" path="/spotify/top/tracks" />
|
||||
|
||||
### 个人 Top Artists
|
||||
|
||||
<Route author="outloudvi" example="/spotify/top/artists" path="/spotify/top/artists" />
|
||||
|
||||
## Sub HD
|
||||
|
||||
### 字幕
|
||||
|
||||
@@ -63,6 +63,11 @@ const calculateValue = () => {
|
||||
disqus: {
|
||||
api_key: envs.DISQUS_API_KEY,
|
||||
},
|
||||
spotify: {
|
||||
clientId: envs.SPOTIFY_CLIENT_ID,
|
||||
clientSecret: envs.SPOTIFY_CLIENT_SECRET,
|
||||
refreshToken: envs.SPOTIFY_REFRESHTOKEN,
|
||||
},
|
||||
twitter: {
|
||||
consumer_key: envs.TWITTER_CONSUMER_KEY,
|
||||
consumer_secret: envs.TWITTER_CONSUMER_SECRET,
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
const utils = require('./utils');
|
||||
const got = require('@/utils/got');
|
||||
const { parseDate } = require('@/utils/parse-date');
|
||||
|
||||
module.exports = async (ctx) => {
|
||||
const token = await utils.getPublicToken();
|
||||
const { id } = ctx.params;
|
||||
const meta = await got
|
||||
.get(`https://api.spotify.com/v1/artists/${id}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
|
||||
const itemsResponse = await got
|
||||
.get(`https://api.spotify.com/v1/artists/${id}/albums`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
const albums = itemsResponse.items;
|
||||
|
||||
ctx.state.data = {
|
||||
title: `Albums of ${meta.name}`,
|
||||
link: meta.external_urls.spotify,
|
||||
allowEmpty: true,
|
||||
item: albums.map((x) => ({
|
||||
title: x.name,
|
||||
author: x.artists.map((a) => a.name).join(', '),
|
||||
description: `"${x.name}" by ${x.artists.map((a) => a.name).join(', ')}, released at ${x.release_date} with ${x.total_tracks} tracks.`,
|
||||
pubDate: parseDate(x.release_date),
|
||||
link: x.external_urls.spotify,
|
||||
})),
|
||||
};
|
||||
|
||||
if (meta.images.length) {
|
||||
ctx.state.data.image = meta.images[0].url;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
'/artist/:id': ['outloudvi'],
|
||||
'/playlist/:id': ['outloudvi'],
|
||||
'/saved/:limit?': ['outloudvi'],
|
||||
'/top/tracks': ['outloudvi'],
|
||||
'/top/artists': ['outloudvi'],
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
const utils = require('./utils');
|
||||
const got = require('@/utils/got');
|
||||
const { parseDate } = require('@/utils/parse-date');
|
||||
|
||||
module.exports = async (ctx) => {
|
||||
const token = await utils.getPublicToken();
|
||||
const { id } = ctx.params;
|
||||
const meta = await got
|
||||
.get(`https://api.spotify.com/v1/playlists/${id}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
const tracks = meta.tracks.items;
|
||||
|
||||
ctx.state.data = {
|
||||
title: meta.name,
|
||||
link: meta.external_urls.spotify,
|
||||
description: meta.description,
|
||||
allowEmpty: true,
|
||||
item: tracks.map((x) => ({
|
||||
...utils.parseTrack(x.track),
|
||||
pubDate: parseDate(x.added_at),
|
||||
})),
|
||||
};
|
||||
|
||||
if (meta.images.length) {
|
||||
ctx.state.data.image = meta.images[0].url;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
module.exports = {
|
||||
'spotify.com': {
|
||||
_name: 'Spotify',
|
||||
open: [
|
||||
{
|
||||
title: '播放列表',
|
||||
docs: 'https://docs.rsshub.app/multimedia.html#spotify',
|
||||
source: ['/playlist/:id'],
|
||||
target: '/spotify/playlist/:id',
|
||||
},
|
||||
{
|
||||
title: '歌手专辑',
|
||||
docs: 'https://docs.rsshub.app/multimedia.html#spotify',
|
||||
source: ['/artist/:id'],
|
||||
target: '/spotify/artist/:id',
|
||||
},
|
||||
{
|
||||
title: '用户 Saved Tracks',
|
||||
docs: 'https://docs.rsshub.app/multimedia.html#spotify',
|
||||
source: ['/collection/tracks'],
|
||||
target: '/spotify/saved',
|
||||
},
|
||||
{
|
||||
title: '用户 Top Tracks',
|
||||
docs: 'https://docs.rsshub.app/multimedia.html#spotify',
|
||||
source: ['/'],
|
||||
target: '/spotify/top/tracks',
|
||||
},
|
||||
{
|
||||
title: '用户 Top Artists',
|
||||
docs: 'https://docs.rsshub.app/multimedia.html#spotify',
|
||||
source: ['/'],
|
||||
target: '/spotify/top/artists',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = function (router) {
|
||||
router.get('/artist/:id', require('./artist'));
|
||||
router.get('/playlist/:id', require('./playlist'));
|
||||
router.get('/saved/:limit?', require('./saved'));
|
||||
router.get('/top/tracks', require('./top')('tracks'));
|
||||
router.get('/top/artists', require('./top')('artists'));
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
const utils = require('./utils');
|
||||
const got = require('@/utils/got');
|
||||
const { parseDate } = require('@/utils/parse-date');
|
||||
|
||||
module.exports = async (ctx) => {
|
||||
const token = await utils.getPrivateToken();
|
||||
|
||||
const { limit } = ctx.params;
|
||||
const pageSize = !isNaN(parseInt(limit)) ? parseInt(limit) : 50;
|
||||
|
||||
const itemsResponse = await got
|
||||
.get(`https://api.spotify.com/v1/me/tracks?limit=${pageSize}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
const tracks = itemsResponse.items;
|
||||
|
||||
ctx.state.data = {
|
||||
title: 'Spotify: My Saved Tracks',
|
||||
link: 'https://open.spotify.com/collection/tracks',
|
||||
description: `Latest ${pageSize} saved tracks on Spotify.`,
|
||||
allowEmpty: true,
|
||||
item: tracks.map((x) => ({
|
||||
...utils.parseTrack(x.track),
|
||||
pubDate: parseDate(x.added_at),
|
||||
})),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
const utils = require('./utils');
|
||||
const got = require('@/utils/got');
|
||||
|
||||
module.exports = (type) => async (ctx) => {
|
||||
if (type !== 'tracks' && type !== 'artists') {
|
||||
throw `Invalid type: ${type}`;
|
||||
}
|
||||
|
||||
const token = await utils.getPrivateToken();
|
||||
const itemsResponse = await got
|
||||
.get(`https://api.spotify.com/v1/me/top/${type}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
const items = itemsResponse.items;
|
||||
|
||||
ctx.state.data = {
|
||||
title: `Spotify: My Top ${type[0].toUpperCase() + type.slice(1)}`,
|
||||
allowEmpty: true,
|
||||
item: type === 'tracks' ? items.map(utils.parseTrack) : items.map(utils.parseArtist),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
const config = require('@/config').value;
|
||||
const got = require('@/utils/got');
|
||||
|
||||
// Token used to retrieve public information.
|
||||
async function getPublicToken() {
|
||||
if (!config.spotify || !config.spotify.clientId || !config.spotify.clientSecret) {
|
||||
throw 'Spotify public RSS is disabled due to the lack of <a href="https://docs.rsshub.app/install/#pei-zhi-bu-fen-rss-mo-kuai-pei-zhi">relevant config</a>';
|
||||
}
|
||||
|
||||
const { clientId, clientSecret } = config.spotify;
|
||||
|
||||
const tokenResponse = await got
|
||||
.post('https://accounts.spotify.com/api/token', {
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`,
|
||||
},
|
||||
form: {
|
||||
grant_type: 'client_credentials',
|
||||
},
|
||||
})
|
||||
.json();
|
||||
return tokenResponse.access_token;
|
||||
}
|
||||
|
||||
// Token used to retrieve user-specific information.
|
||||
// Note that we don't use PKCE since the client secret shall be safe on the server.
|
||||
async function getPrivateToken() {
|
||||
if (!config.spotify || !config.spotify.clientId || !config.spotify.clientSecret || !config.spotify.refreshToken) {
|
||||
throw 'Spotify private RSS is disabled due to the lack of <a href="https://docs.rsshub.app/install/#pei-zhi-bu-fen-rss-mo-kuai-pei-zhi">relevant config</a>';
|
||||
}
|
||||
|
||||
const { clientId, clientSecret, refreshToken } = config.spotify;
|
||||
|
||||
const tokenResponse = await got
|
||||
.post('https://accounts.spotify.com/api/token', {
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`,
|
||||
},
|
||||
form: {
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: refreshToken,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
return tokenResponse.access_token;
|
||||
}
|
||||
|
||||
const parseTrack = (x) => ({
|
||||
title: x.name,
|
||||
author: x.artists.map((a) => a.name).join(', '),
|
||||
description: `"${x.name}" by ${x.artists.map((a) => a.name).join(', ')} from the album "${x.album.name}"`,
|
||||
link: x.external_urls.spotify,
|
||||
});
|
||||
|
||||
const parseArtist = (x) => ({ title: x.name, description: `${x.name}, with ${x.followers.total} followers`, link: x.external_urls.spotify });
|
||||
|
||||
module.exports = {
|
||||
getPublicToken,
|
||||
getPrivateToken,
|
||||
parseTrack,
|
||||
parseArtist,
|
||||
};
|
||||
Reference in New Issue
Block a user