Skip to content
Snippets Groups Projects
Verified Commit 5021ba4f authored by Eliot Berriot's avatar Eliot Berriot
Browse files

Added support for Funkwhale

parent feda5102
No related branches found
No related tags found
No related merge requests found
Pipeline #4244 passed
src/assets/sources/funkwhale-logo.png

40 KiB

<template>
<form @submit.prevent="submit">
<div class="row">
<div class="input-field col s12 l6">
<input v-model="domain" id="domain" placeholder="funkwhale.domain" type="text" class="validate">
<label class="active" for="domain">Funkwhale domain</label>
</div>
</div>
<button type="submit" class="waves-effect waves-light btn">Authenticate</button>
</form>
</template>
<script>
import axios from 'axios'
import {createApp, getAxios, SCOPES} from '@/sources/funkwhale'
import {getBaseUrl} from '@/utils'
export default {
props: ["source"],
data () {
return {
domain: '',
}
},
mounted () {
M.updateTextFields();
},
methods: {
async submit () {
let instanceUrl = `https://${this.domain}/`
let baseUrl = getBaseUrl(window.location.toString(), this.$router.currentRoute.path)
// create an application
let ax = axios.create({
baseURL: instanceUrl,
timeout: 3000,
})
let response = await createApp(ax, {baseUrl})
const appData = response.data
const redirectUri = encodeURIComponent(`${baseUrl}/connect/funkwhale/callback`)
this.$store.commit('setRecursiveState', {key: "sources.funkwhale.appCredentials", suffix: this.domain, value: appData})
let params = `response_type=code&scope=${encodeURIComponent(SCOPES)}&redirect_uri=${redirectUri}&state=${this.domain}&client_id=${appData.client_id}`
const authorizeUrl = `${instanceUrl}authorize?${params}`
console.log('Redirecting user...', authorizeUrl)
window.location = authorizeUrl
}
}
}
</script>
...@@ -43,8 +43,11 @@ ...@@ -43,8 +43,11 @@
<div v-if="isLoading" class="progress"> <div v-if="isLoading" class="progress">
<div class="indeterminate"></div> <div class="indeterminate"></div>
</div> </div>
<p> <p v-if="filters.retributeOnly">
{{ filteredSuggestions.length }} profiles found with retribute information ({{ allSuggestions.length }} in total) {{ filteredSuggestions.length }} profiles found with retribute information (<a @click.prevent="filters.retributeOnly = null">show all {{ allSuggestions.length }} profiles</a>)
</p>
<p v-else>
{{ filteredSuggestions.length }} profiles found (<a @click.prevent="filters.retributeOnly = true">restrict to profiles with retribute information</a>)
</p> </p>
<ul class="collection"> <ul class="collection">
<li class="collection-item avatar" v-for="suggestion in filteredSuggestions" :key="suggestion.fullId"> <li class="collection-item avatar" v-for="suggestion in filteredSuggestions" :key="suggestion.fullId">
...@@ -97,7 +100,7 @@ export default { ...@@ -97,7 +100,7 @@ export default {
retributeProfiles: this.$store.state.cache.retributeProfiles || {}, retributeProfiles: this.$store.state.cache.retributeProfiles || {},
loadingRetributeProfiles: [], loadingRetributeProfiles: [],
filters: { filters: {
retributeOnly: true, retributeOnly: null,
providers: [], providers: [],
}, },
providers: this.$store.state.cache.providers || [] providers: this.$store.state.cache.providers || []
......
import axios from 'axios'
import moment from 'moment'
import Form from '../components/FunkwhaleForm.vue'
import get from 'lodash/get'
import {getAbsoluteUrl} from "@/utils"
export const SCOPES = "read:profile read:listenings"
export function listeningsToRetribute (listenings, cache) {
if (!cache.profiles) {
cache.profiles = {}
}
listenings.forEach(f => {
let mbid = f.track.artist.mbid
if (!mbid) {
return
}
const profileId = `musicbrainz:${mbid}`
if (cache.profiles[profileId]) {
cache.profiles[profileId].weight += 1
} else {
cache.profiles[profileId] = {weight: 1}
}
});
return cache
}
export async function createApp(ax, {baseUrl}) {
const payload = {
name: `Retribute ${window.location.hostname}`,
website: baseUrl,
scopes: SCOPES,
redirect_uris: `${baseUrl}/connect/funkwhale/callback`
}
const response = await ax.post('/api/v1/oauth/apps/', payload)
return response
}
export default {
id: "funkwhale",
label: "Funkwhale",
url: "https://funkwhale.audio",
description: "Funkwhale servers",
extendedDescription: "Receive suggestions based on your listening history",
imageBackground: "grey lighten-5",
form: Form,
getLogo () {
return require("../assets/sources/funkwhale-logo.png")
},
authDataToKey ({username, domain}) {
return `funkwhale ${domain} ${username}`
},
getAvatar (data) {
if (!data.raw || !data.raw.avatar) {
return null
}
let url = data.raw.avatar.small_square_crop
return getAbsoluteUrl(`https://${data.domain}`, url)
},
getDisplayName ({username, domain}) {
return `${username}@${domain}`
},
async handleCallback({query, router, store, baseUrl}) {
const domain = query.state
console.log(`Received connect callback for funkwhale domain ${domain}`)
console.log('Fetching token...')
const app = store.state.sources.funkwhale.appCredentials[domain]
const ax = axios.create()
const payload = {
client_id: app.client_id,
client_secret: app.client_secret,
grant_type: "authorization_code",
code: query.code,
redirect_uri: `${baseUrl}/connect/funkwhale/callback`
}
let data = new FormData()
Object.entries(payload).forEach((e) => {
data.set(e[0], e[1])
})
const response = await ax.post(`https://${domain}/api/v1/oauth/token/`, data, {headers: {'Content-Type': 'multipart/form-data'}})
const token = response.data.access_token
const client = axios.create({
baseURL: `https://${domain}`,
headers: {'Authorization': `Bearer ${token}`},
})
const accountResponse = await client.get('/api/v1/users/users/me/')
const username = `${accountResponse.data.username}@${domain}`
store.commit('setRecursiveState', {key: "sources.funkwhale.appTokens", suffix: username, value: response.data})
store.commit('addAccount', {source: 'funkwhale', raw: accountResponse.data, username: accountResponse.data.username, domain})
router.push({path: '/'})
},
async fetch ({account, store, results, maxDays}) {
results.status = `Fetching listenings...`
const token = store.state.sources.funkwhale.appTokens[`${account.username}@${account.domain}`].access_token
const client = axios.create({
baseURL: `https://${account.domain}`,
headers: {'Authorization': `Bearer ${token}`},
})
const dateLimit = moment().subtract(maxDays, 'days')
let url = null
let handledListenings = 0
// results.progress = 0
// results.progressCount
results.accounts = {}
let cont = true
while (cont) {
let response
if (!url) {
response = await client.get('/api/v1/history/listenings/', {params: {page_size: 100, username: account.username, domain: account.domain}})
} else {
response = await client.get(url)
}
response.data.results.forEach((l) => {
let date = moment(l.creation_date)
if (date.isBefore(dateLimit)) {
cont = false
return
}
if (account.username != l.user.username) {
// we don't have a ?user= filter on this API, so we exclude listenings by hand :s
return
}
handledListenings += 1
results.progressCount += 1
let artist = l.track.artist
if (!artist.mbid) {
return
}
let artistId = `musicbrainz:${artist.mbid}`
if (results.accounts[artistId]) {
results.accounts[artistId].weight += 1
let detail = get(results.accounts[artistId], 'detail', {})
let mastodonDetail = get(detail, 'funkwhale', {listenings: 0})
mastodonDetail.listenings += 1
} else {
results.accounts[artistId] = {
weight: 1,
source: 'musicbrainz',
id: artistId,
avatar: null,
name: artist.name,
url: `https://musicbrainz.org/artist/${artist.mbid}`,
detail: {
funkwhale: {
listenings: 1
}
}
}
}
// results.progress = Math.min(100, handledListenings / maxFavorites * 100)
})
if (response.data.next) {
url = response.data.next
} else {
break
}
results.status = `Fetched ${handledListenings} listenings`
}
results.isLoading = false
return results
},
getDetailMessage (detail) {
if (!detail) {
return
}
let listenings = detail.listenings || 0
return `Funkwhale: ${listenings} listenings`
}
}
import Mastodon from "./mastodon" import Mastodon from "./mastodon"
import Funkwhale from "./funkwhale"
import sortBy from "lodash/sortBy" import sortBy from "lodash/sortBy"
...@@ -7,6 +8,7 @@ export default { ...@@ -7,6 +8,7 @@ export default {
return sortBy(Object.values(this.sources), ["id"]) return sortBy(Object.values(this.sources), ["id"])
}, },
sources: { sources: {
funkwhale: Funkwhale,
mastodon: Mastodon mastodon: Mastodon
} }
} }
...@@ -11,3 +11,14 @@ export function getBaseUrl (windowUrl, currentPath) { ...@@ -11,3 +11,14 @@ export function getBaseUrl (windowUrl, currentPath) {
} }
return url return url
} }
export function getAbsoluteUrl (base, relative) {
if (relative.startsWith('http')) {
return relative
}
if (base.endsWith('/') && relative.startsWith('/')) {
relative = relative.slice(1)
}
return base + relative
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment