feat(plugin/music): add support for new top mode with top artists or tracks (#569)
This commit is contained in:
@@ -62,5 +62,99 @@ export default function({faker, url, options, login = faker.internet.userName()}
|
||||
},
|
||||
})
|
||||
}
|
||||
else if (/user.gettoptracks/.test(url)) {
|
||||
console.debug(`metrics/compute/mocks > mocking lastfm api result > ${url}`)
|
||||
const artist = faker.random.word()
|
||||
const track = faker.random.words(5)
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
toptracks:{
|
||||
"@attr":{
|
||||
page:"1",
|
||||
perPage:"1",
|
||||
user:"RJ",
|
||||
total:"100",
|
||||
pages:"100",
|
||||
},
|
||||
track:[
|
||||
{
|
||||
artist:{
|
||||
mbid:"",
|
||||
name:artist,
|
||||
},
|
||||
image:[
|
||||
{
|
||||
size:"small",
|
||||
"#text":faker.image.abstract(),
|
||||
},
|
||||
{
|
||||
size:"medium",
|
||||
"#text":faker.image.abstract(),
|
||||
},
|
||||
{
|
||||
size:"large",
|
||||
"#text":faker.image.abstract(),
|
||||
},
|
||||
{
|
||||
size:"extralarge",
|
||||
"#text":faker.image.abstract(),
|
||||
},
|
||||
],
|
||||
url:faker.internet.url(),
|
||||
name:track,
|
||||
mbid:"",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
else if (/user.gettopartists/.test(url)) {
|
||||
console.debug(`metrics/compute/mocks > mocking lastfm api result > ${url}`)
|
||||
const artist = faker.random.word()
|
||||
const playcount = faker.random.number()
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
topartists:{
|
||||
"@attr":{
|
||||
page:"1",
|
||||
perPage:"1",
|
||||
user:"RJ",
|
||||
total:"100",
|
||||
pages:"100",
|
||||
},
|
||||
artist:[
|
||||
{
|
||||
image:[
|
||||
{
|
||||
size:"small",
|
||||
"#text":faker.image.abstract(),
|
||||
},
|
||||
{
|
||||
size:"medium",
|
||||
"#text":faker.image.abstract(),
|
||||
},
|
||||
{
|
||||
size:"large",
|
||||
"#text":faker.image.abstract(),
|
||||
},
|
||||
{
|
||||
size:"extralarge",
|
||||
"#text":faker.image.abstract(),
|
||||
},
|
||||
],
|
||||
streamable:"0",
|
||||
playcount,
|
||||
url:faker.internet.url(),
|
||||
name:artist,
|
||||
mbid:"",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,5 +61,91 @@ export default function({faker, url, options, login = faker.internet.userName()}
|
||||
},
|
||||
})
|
||||
}
|
||||
else if (/me.top.tracks/.test(url) && (options?.headers?.Authorization === "Bearer MOCKED_TOKEN_ACCESS")) {
|
||||
console.debug(`metrics/compute/mocks > mocking spotify api result > ${url}`)
|
||||
const artist = faker.random.words()
|
||||
const track = faker.random.words(5)
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
items:[
|
||||
{
|
||||
album:{
|
||||
album_type:"single",
|
||||
artists:[
|
||||
{
|
||||
name:artist,
|
||||
type:"artist",
|
||||
},
|
||||
],
|
||||
images:[
|
||||
{
|
||||
height:640,
|
||||
url:faker.image.abstract(),
|
||||
width:640,
|
||||
},
|
||||
{
|
||||
height:300,
|
||||
url:faker.image.abstract(),
|
||||
width:300,
|
||||
},
|
||||
{
|
||||
height:64,
|
||||
url:faker.image.abstract(),
|
||||
width:64,
|
||||
},
|
||||
],
|
||||
name:track,
|
||||
release_date:`${faker.date.past()}`.substring(0, 10),
|
||||
type:"album",
|
||||
},
|
||||
artists:[
|
||||
{
|
||||
name:artist,
|
||||
type:"artist",
|
||||
},
|
||||
],
|
||||
name:track,
|
||||
preview_url:faker.internet.url(),
|
||||
type:"track",
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
else if (/me.top.artists/.test(url) && (options?.headers?.Authorization === "Bearer MOCKED_TOKEN_ACCESS")) {
|
||||
console.debug(`metrics/compute/mocks > mocking spotify api result > ${url}`)
|
||||
const genre = faker.random.words()
|
||||
const track = faker.random.words(5)
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
items:[
|
||||
{
|
||||
genres: [genre],
|
||||
images:[
|
||||
{
|
||||
height:640,
|
||||
url:faker.image.abstract(),
|
||||
width:640,
|
||||
},
|
||||
{
|
||||
height:300,
|
||||
url:faker.image.abstract(),
|
||||
width:300,
|
||||
},
|
||||
{
|
||||
height:64,
|
||||
url:faker.image.abstract(),
|
||||
width:64,
|
||||
},
|
||||
],
|
||||
name:track,
|
||||
type:"artist",
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,9 +81,11 @@ This mode is not supported for now.
|
||||
# (plugin_music_provider and plugin_music_mode will be set automatically)
|
||||
```
|
||||
|
||||
### Recently played mode
|
||||
### Recently played & top modes
|
||||
|
||||
Display tracks you have played recently.
|
||||
Recently played: Display tracks you have played recently.
|
||||
|
||||
Top: Display your top artists/tracks for a certain time period.
|
||||
|
||||
Select a music provider below for additional instructions.
|
||||
|
||||
@@ -113,7 +115,7 @@ Open the settings and add a new *Redirect url*. Normally it is used to setup cal
|
||||
Forge the authorization url with your `client_id` and the encoded `redirect_uri` you whitelisted, and access it from your browser:
|
||||
|
||||
```
|
||||
https://accounts.spotify.com/authorize?client_id=********&response_type=code&scope=user-read-recently-played&redirect_uri=https%3A%2F%2Flocalhost
|
||||
https://accounts.spotify.com/authorize?client_id=********&response_type=code&scope=user-read-recently-played%20user-top-read&redirect_uri=https%3A%2F%2Flocalhost
|
||||
```
|
||||
|
||||
When prompted, authorize your application.
|
||||
@@ -147,7 +149,7 @@ It should return a JSON response with the following content:
|
||||
{
|
||||
"access_token":"********",
|
||||
"expires_in": 3600,
|
||||
"scope":"user-read-recently-played",
|
||||
"scope":"user-read-recently-played user-top-read",
|
||||
"token_type":"Bearer",
|
||||
"refresh_token":"********"
|
||||
}
|
||||
@@ -172,6 +174,8 @@ Register your API key to finish setup.
|
||||
|
||||
[➡️ Available options for this plugin](metadata.yml)
|
||||
|
||||
##### Recent
|
||||
|
||||
```yaml
|
||||
- uses: lowlighter/metrics@latest
|
||||
with:
|
||||
@@ -196,3 +200,33 @@ Register your API key to finish setup.
|
||||
plugin_music_token: ${{ secrets.LASTFM_API_KEY }}
|
||||
|
||||
```
|
||||
|
||||
##### Top
|
||||
|
||||
```yaml
|
||||
- uses: lowlighter/metrics@latest
|
||||
with:
|
||||
# ... other options
|
||||
plugin_music: yes
|
||||
plugin_music_provider: spotify # Use Spotify as provider
|
||||
plugin_music_mode: top # Set plugin mode
|
||||
plugin_music_top_type: tracks # Set type for "top" mode; either tracks or artists
|
||||
plugin_music_limit: 4 # Limit to 4 entries, maximum is 50 for "top" mode with spotify
|
||||
plugin_music_time_range: short # Set time range for "top" mode; either short (4 weeks), medium (6 months) or long (several years)
|
||||
plugin_music_token: "${{ secrets.SPOTIFY_CLIENT_ID }}, ${{ secrets.SPOTIFY_CLIENT_SECRET }}, ${{ secrets.SPOTIFY_REFRESH_TOKEN }}"
|
||||
```
|
||||
|
||||
```yaml
|
||||
- uses: lowlighter/metrics@latest
|
||||
with:
|
||||
# ... other options
|
||||
plugin_music: yes
|
||||
plugin_music_provider: lastfm # Use Last.fm as provider
|
||||
plugin_music_mode: top # Set plugin mode
|
||||
plugin_music_top_type: artists # Set type for "top" mode; either tracks or artists
|
||||
plugin_music_limit: 4 # Limit to 4 entries
|
||||
plugin_music_time_range: long # Set time range for "top" mode; either short (4 weeks), medium (6 months) or long (several years)
|
||||
plugin_music_user: .user.login # Use same username as GitHub login
|
||||
plugin_music_token: ${{ secrets.LASTFM_API_KEY }}
|
||||
|
||||
```
|
||||
|
||||
@@ -17,6 +17,7 @@ const providers = {
|
||||
const modes = {
|
||||
playlist:"Suggested tracks",
|
||||
recent:"Recently played",
|
||||
top:"Top played",
|
||||
}
|
||||
|
||||
//Setup
|
||||
@@ -39,18 +40,23 @@ export default async function({login, imports, data, q, account}, {enabled = fal
|
||||
let tracks = null
|
||||
|
||||
//Load inputs
|
||||
let {provider, mode, playlist, limit, user, "played.at":played_at} = imports.metadata.plugins.music.inputs({data, account, q})
|
||||
let {provider, mode, playlist, limit, user, "played.at":played_at, "time.range":time_range, "top.type":top_type} = imports.metadata.plugins.music.inputs({data, account, q})
|
||||
//Auto-guess parameters
|
||||
if ((playlist) && (!mode))
|
||||
if (!mode) {
|
||||
if (playlist) {
|
||||
mode = "playlist"
|
||||
if ((playlist) && (!provider)) {
|
||||
if (!provider) {
|
||||
for (const [name, {embed}] of Object.entries(providers)) {
|
||||
if (embed.test(playlist))
|
||||
provider = name
|
||||
}
|
||||
}
|
||||
if (!mode)
|
||||
}
|
||||
else if ("music.top.type" in q || "music.time.range" in q)
|
||||
mode = "top"
|
||||
else
|
||||
mode = "recent"
|
||||
}
|
||||
//Provider
|
||||
if (!(provider in providers))
|
||||
throw {error:{message:provider ? `Unsupported provider "${provider}"` : "Missing provider"}, ...raw}
|
||||
@@ -224,6 +230,172 @@ export default async function({login, imports, data, q, account}, {enabled = fal
|
||||
}
|
||||
break
|
||||
}
|
||||
case "top": {
|
||||
let time_msg
|
||||
switch (time_range) {
|
||||
case "short":
|
||||
time_msg = "from the last month"
|
||||
break
|
||||
case "medium":
|
||||
time_msg = "from the last 6 months"
|
||||
break
|
||||
case "long":
|
||||
time_msg = "overall"
|
||||
break
|
||||
default:
|
||||
throw {error:{message:`Unsupported time range "${time_range}"`}, ...raw}
|
||||
}
|
||||
|
||||
if (top_type === "artists") {
|
||||
Object.defineProperty(modes, "top", {
|
||||
get() {
|
||||
return `Top played artists ${time_msg}`
|
||||
}
|
||||
})
|
||||
}
|
||||
else {
|
||||
Object.defineProperty(modes, "top", {
|
||||
get() {
|
||||
return `Top played tracks ${time_msg}`
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
//Handle provider
|
||||
switch (provider) {
|
||||
//Spotify
|
||||
case "spotify": {
|
||||
//Prepare credentials
|
||||
const [client_id, client_secret, refresh_token] = token.split(",").map(part => part.trim())
|
||||
if ((!client_id) || (!client_secret) || (!refresh_token))
|
||||
throw { error: { message: "Spotify token must contain client id/secret and refresh token" } }
|
||||
else if (limit > 50)
|
||||
throw {error:{message:"Spotify top limit cannot be greater than 50"}}
|
||||
|
||||
//API call and parse tracklist
|
||||
try {
|
||||
//Request access token
|
||||
console.debug(`metrics/compute/${login}/plugins > music > requesting access token with spotify refresh token`)
|
||||
const {data:{access_token:access}} = await imports.axios.post("https://accounts.spotify.com/api/token", `${new imports.url.URLSearchParams({grant_type:"refresh_token", refresh_token, client_id, client_secret})}`, {
|
||||
headers:{
|
||||
"Content-Type":"application/x-www-form-urlencoded",
|
||||
},
|
||||
})
|
||||
console.debug(`metrics/compute/${login}/plugins > music > got access token`)
|
||||
//Retrieve tracks
|
||||
console.debug(`metrics/compute/${login}/plugins > music > querying spotify api`)
|
||||
tracks = []
|
||||
const loaded =
|
||||
top_type === "artists"
|
||||
? (
|
||||
await imports.axios.get(
|
||||
`https://api.spotify.com/v1/me/top/artists?time_range=${time_range}_term&limit=${limit}`,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
Authorization: `Bearer ${access}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
).data.items.map(({ name, genres, images }) => ({
|
||||
name,
|
||||
artist: genres.join(" • "),
|
||||
artwork: images[0].url,
|
||||
}))
|
||||
: (
|
||||
await imports.axios.get(
|
||||
`https://api.spotify.com/v1/me/top/tracks?time_range=${time_range}_term&limit=${limit}`,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
Authorization: `Bearer ${access}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
).data.items.map(({ name, artists, album }) => ({
|
||||
name,
|
||||
artist: artists[0].name,
|
||||
artwork: album.images[0].url,
|
||||
}))
|
||||
//Ensure no duplicate are added
|
||||
for (const track of loaded) {
|
||||
if (!tracks.map(({name}) => name).includes(track.name))
|
||||
tracks.push(track)
|
||||
}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.isAxiosError) {
|
||||
const status = error.response?.status
|
||||
const description = error.response.data?.error_description ?? null
|
||||
const message = `API returned ${status}${description ? ` (${description})` : ""}`
|
||||
error = error.response?.data ?? null
|
||||
throw {error:{message, instance:error}, ...raw}
|
||||
}
|
||||
throw error
|
||||
}
|
||||
break
|
||||
}
|
||||
//Last.fm
|
||||
case "lastfm": {
|
||||
//API call and parse tracklist
|
||||
try {
|
||||
console.debug(`metrics/compute/${login}/plugins > music > querying lastfm api`)
|
||||
const period = time_range === "short" ? "1month" : time_range === "medium" ? "6month" : "overall"
|
||||
tracks =
|
||||
top_type === "artists"
|
||||
? (
|
||||
await imports.axios.get(
|
||||
`https://ws.audioscrobbler.com/2.0/?method=user.gettopartists&user=${user}&api_key=${token}&limit=${limit}&period=${period}&format=json`,
|
||||
{
|
||||
headers: {
|
||||
"User-Agent": "lowlighter/metrics",
|
||||
Accept: "application/json",
|
||||
},
|
||||
}
|
||||
)
|
||||
).data.topartists.artist.map(artist => ({
|
||||
name: artist.name,
|
||||
artist: `Play count: ${artist.playcount}`,
|
||||
artwork: artist.image.reverse()[0]["#text"],
|
||||
}))
|
||||
: (
|
||||
await imports.axios.get(
|
||||
`https://ws.audioscrobbler.com/2.0/?method=user.gettoptracks&user=${user}&api_key=${token}&limit=${limit}&period=${period}&format=json`,
|
||||
{
|
||||
headers: {
|
||||
"User-Agent": "lowlighter/metrics",
|
||||
Accept: "application/json",
|
||||
},
|
||||
}
|
||||
)
|
||||
).data.toptracks.track.map(track => ({
|
||||
name: track.name,
|
||||
artist: track.artist.name,
|
||||
artwork: track.image.reverse()[0]["#text"],
|
||||
}))
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.isAxiosError) {
|
||||
const status = error.response?.status
|
||||
const description = error.response.data?.message ?? null
|
||||
const message = `API returned ${status}${description ? ` (${description})` : ""}`
|
||||
error = error.response?.data ?? null
|
||||
throw {error:{message, instance:error}, ...raw}
|
||||
}
|
||||
throw error
|
||||
}
|
||||
break
|
||||
}
|
||||
//Unsupported
|
||||
default:
|
||||
throw {error:{message:`Unsupported mode "${mode}" for provider "${provider}"`}, ...raw}
|
||||
}
|
||||
break
|
||||
}
|
||||
//Unsupported
|
||||
default:
|
||||
throw {error:{message:`Unsupported mode "${mode}"`}, ...raw}
|
||||
|
||||
@@ -28,7 +28,7 @@ inputs:
|
||||
# Music provider token
|
||||
# This may be required depending on music provider used and plugin mode
|
||||
# - "apple" : not required
|
||||
# - "spotify" : required for "recent" mode, format is "client_id, client_secret, refresh_token"
|
||||
# - "spotify" : required for "recent" or "top" mode, format is "client_id, client_secret, refresh_token"
|
||||
# - "lastfm" : required, format is "api_key"
|
||||
plugin_music_token:
|
||||
description: Music provider personal token
|
||||
@@ -43,6 +43,7 @@ inputs:
|
||||
values:
|
||||
- playlist # Display tracks from an embed playlist randomly
|
||||
- recent # Display recently listened tracks
|
||||
- top # Display top listened artists/tracks
|
||||
|
||||
# Embed playlist url (i.e. url used by music player iframes)
|
||||
plugin_music_playlist:
|
||||
@@ -65,6 +66,25 @@ inputs:
|
||||
type: boolean
|
||||
default: no
|
||||
|
||||
# Time range for "top" mode
|
||||
plugin_music_time_range:
|
||||
description: Time period for top mode
|
||||
type: string
|
||||
default: short # Defaults to "short" (4 weeks)
|
||||
values:
|
||||
- short # Top artists/tracks from past 4 weeks
|
||||
- medium # Top artists/tracks from past 6 months
|
||||
- long # Top artists/tracks from several years
|
||||
|
||||
# Option for "top" mode to select tracks or artists
|
||||
plugin_music_top_type:
|
||||
description: Whether to show tracks or artists in top mode
|
||||
type: string
|
||||
default: tracks
|
||||
values:
|
||||
- tracks
|
||||
- artists
|
||||
|
||||
# Username on music provider service
|
||||
plugin_music_user:
|
||||
description: Music provider username
|
||||
|
||||
@@ -29,3 +29,25 @@
|
||||
plugin_music: yes
|
||||
plugin_music_provider: lastfm
|
||||
plugin_music_user: RJ
|
||||
|
||||
- name: Music plugin (top - spotify - tracks)
|
||||
uses: lowlighter/metrics@latest
|
||||
with:
|
||||
token: NOT_NEEDED
|
||||
plugin_music_token: MOCKED_CLIENT_ID, MOCKED_CLIENT_SECRET, MOCKED_REFRESH_TOKEN
|
||||
plugin_music: yes
|
||||
plugin_music_mode: top
|
||||
plugin_music_provider: spotify
|
||||
plugin_music_time_range: short
|
||||
plugin_music_top_type: tracks
|
||||
|
||||
- name: Music plugin (top - spotify - artists)
|
||||
uses: lowlighter/metrics@latest
|
||||
with:
|
||||
token: NOT_NEEDED
|
||||
plugin_music_token: MOCKED_CLIENT_ID, MOCKED_CLIENT_SECRET, MOCKED_REFRESH_TOKEN
|
||||
plugin_music: yes
|
||||
plugin_music_mode: top
|
||||
plugin_music_provider: spotify
|
||||
plugin_music_time_range: long
|
||||
plugin_music_top_type: artists
|
||||
|
||||
@@ -22,11 +22,11 @@
|
||||
<div class="tracklist">
|
||||
<% for (const {name = "", artist = "", artwork = "", played_at = ""} of plugins.music.tracks) { %>
|
||||
<div class="track">
|
||||
<img src="<%= artwork %>" width="<%= plugins.music.played_at ? 48 : 32 %>" height="<%= plugins.music.played_at ? 48 : 32 %>" alt=""/>
|
||||
<img src="<%= artwork %>" width="<%= plugins.music.played_at && played_at ? 48 : 32 %>" height="<%= plugins.music.played_at && played_at ? 48 : 32 %>" alt=""/>
|
||||
<div class="infos">
|
||||
<div class="name"><%= name %></div>
|
||||
<div class="artist"><%= artist %></div>
|
||||
<% if (plugins.music.played_at) { %>
|
||||
<% if (plugins.music.played_at && played_at) { %>
|
||||
<div class="played-at">Played at <%= played_at %></div>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user