diff --git a/src/assets/sources/funkwhale-logo.png b/src/assets/sources/funkwhale-logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..725dcc8acfcc2165751caf98b6fee964832b4677
Binary files /dev/null and b/src/assets/sources/funkwhale-logo.png differ
diff --git a/src/components/FunkwhaleForm.vue b/src/components/FunkwhaleForm.vue
new file mode 100644
index 0000000000000000000000000000000000000000..5e5f86771b1875cbfa0c1cbab40f2bf18fda0e2d
--- /dev/null
+++ b/src/components/FunkwhaleForm.vue
@@ -0,0 +1,50 @@
+<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>
diff --git a/src/components/Suggestions.vue b/src/components/Suggestions.vue
index 241550ee6bc4c0e8f4dddd8a6b911fd75f17be32..422106c19d239f6e6d77c3ece40488c7744ebbec 100644
--- a/src/components/Suggestions.vue
+++ b/src/components/Suggestions.vue
@@ -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 || []
diff --git a/src/sources/funkwhale.js b/src/sources/funkwhale.js
new file mode 100644
index 0000000000000000000000000000000000000000..dd650911b82778af6679a3cb0a39ec380fa84519
--- /dev/null
+++ b/src/sources/funkwhale.js
@@ -0,0 +1,174 @@
+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`
+  }
+}
diff --git a/src/sources/index.js b/src/sources/index.js
index 43e6b9f00d4c72b6fb973cb4b9e9da27717b6294..299f3abac5ba1144e5e014c158178c3bf474f178 100644
--- a/src/sources/index.js
+++ b/src/sources/index.js
@@ -1,4 +1,5 @@
 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
   }
 }
diff --git a/src/utils.js b/src/utils.js
index 0351fbd4860f20c264d792520878970704d97850..633721e12ba94bd2a90defa11779e91bdc38b280 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -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
+}