Verified Commit 5021ba4f authored by Eliot Berriot's avatar Eliot Berriot
Browse files

Added support for Funkwhale

parent feda5102
Pipeline #4244 passed with stages
in 1 minute and 22 seconds
<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 @@
<div v-if="isLoading" class="progress">
<div class="indeterminate"></div>
</div>
<p>
{{ filteredSuggestions.length }} profiles found with retribute information ({{ allSuggestions.length }} in total)
<p v-if="filters.retributeOnly">
{{ 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>
<ul class="collection">
<li class="collection-item avatar" v-for="suggestion in filteredSuggestions" :key="suggestion.fullId">
......@@ -97,7 +100,7 @@ export default {
retributeProfiles: this.$store.state.cache.retributeProfiles || {},
loadingRetributeProfiles: [],
filters: {
retributeOnly: true,
retributeOnly: null,
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 Funkwhale from "./funkwhale"
import sortBy from "lodash/sortBy"
......@@ -7,6 +8,7 @@ export default {
return sortBy(Object.values(this.sources), ["id"])
},
sources: {
funkwhale: Funkwhale,
mastodon: Mastodon
}
}
......@@ -11,3 +11,14 @@ export function getBaseUrl (windowUrl, currentPath) {
}
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
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment