diff --git a/api/funkwhale_api/federation/models.py b/api/funkwhale_api/federation/models.py
index 76dbfd1ade0779965b09f7575107e9b4bebaf7f4..e841b639489c754a92d426da18c38445c3e1d7e8 100644
--- a/api/funkwhale_api/federation/models.py
+++ b/api/funkwhale_api/federation/models.py
@@ -163,3 +163,10 @@ class LibraryTrack(models.Model):
     title = models.CharField(max_length=500)
     metadata = JSONField(
         default={}, max_length=10000, encoder=DjangoJSONEncoder)
+
+    @property
+    def mbid(self):
+        try:
+            return self.metadata['recording']['musicbrainz_id']
+        except KeyError:
+            pass
diff --git a/api/funkwhale_api/music/forms.py b/api/funkwhale_api/music/forms.py
index 04e4bfe057c0723b161265c63000a2494c687a9d..e68ab73cc2b95030d6dfb284a10b8becbefc9a9e 100644
--- a/api/funkwhale_api/music/forms.py
+++ b/api/funkwhale_api/music/forms.py
@@ -19,5 +19,5 @@ class TranscodeForm(forms.Form):
         choices=BITRATE_CHOICES, required=False)
 
     track_file = forms.ModelChoiceField(
-        queryset=models.TrackFile.objects.all()
+        queryset=models.TrackFile.objects.exclude(audio_file__isnull=True)
     )
diff --git a/api/funkwhale_api/music/serializers.py b/api/funkwhale_api/music/serializers.py
index 42795dbea87b8114125dea4e8153eabd5c2600e5..b5f69eb1db866225fca4cc147e71a08901194dd8 100644
--- a/api/funkwhale_api/music/serializers.py
+++ b/api/funkwhale_api/music/serializers.py
@@ -3,8 +3,9 @@ from rest_framework import serializers
 from taggit.models import Tag
 
 from funkwhale_api.activity import serializers as activity_serializers
-from funkwhale_api.federation.serializers import AP_CONTEXT
 from funkwhale_api.federation import utils as federation_utils
+from funkwhale_api.federation.models import LibraryTrack
+from funkwhale_api.federation.serializers import AP_CONTEXT
 
 from . import models
 
@@ -153,3 +154,25 @@ class TrackActivitySerializer(activity_serializers.ModelSerializer):
 
     def get_type(self, obj):
         return 'Audio'
+
+
+class SubmitFederationTracksSerializer(serializers.Serializer):
+    library_tracks = serializers.PrimaryKeyRelatedField(
+        many=True,
+        queryset=LibraryTrack.objects.filter(local_track_file__isnull=True),
+    )
+
+    @transaction.atomic
+    def save(self, **kwargs):
+        batch = models.ImportBatch.objects.create(
+            source='federation',
+            **kwargs
+        )
+        for lt in self.validated_data['library_tracks']:
+            models.ImportJob.objects.create(
+                batch=batch,
+                library_track=lt,
+                mbid=lt.mbid,
+                source=lt.url,
+            )
+        return batch
diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py
index 98048b41d7edc41ac28749172feda426aaa03708..d5247fbf60c8ad2419c5a467c6d4a4ef9d145431 100644
--- a/api/funkwhale_api/music/views.py
+++ b/api/funkwhale_api/music/views.py
@@ -1,6 +1,7 @@
 import ffmpeg
 import os
 import json
+import logging
 import subprocess
 import unicodedata
 import urllib
@@ -40,6 +41,8 @@ from . import serializers
 from . import tasks
 from . import utils
 
+logger = logging.getLogger(__name__)
+
 
 class SearchMixin(object):
     search_fields = []
@@ -223,6 +226,8 @@ class TrackFileViewSet(viewsets.ReadOnlyModelViewSet):
                 headers={
                     'Content-Type': 'application/activity+json'
                 })
+            logger.debug(
+                'Proxying media request to %s', library_track.audio_url)
             response = StreamingHttpResponse(remote_response.iter_content())
         else:
             response = Response()
@@ -249,6 +254,8 @@ class TrackFileViewSet(viewsets.ReadOnlyModelViewSet):
             return Response(form.errors, status=400)
 
         f = form.cleaned_data['track_file']
+        if not f.audio_file:
+            return Response(status=400)
         output_kwargs = {
             'format': form.cleaned_data['to']
         }
@@ -392,6 +399,22 @@ class SubmitViewSet(viewsets.ViewSet):
             data, request, batch=None, import_request=import_request)
         return Response(import_data)
 
+    @list_route(methods=['post'])
+    @transaction.non_atomic_requests
+    def federation(self, request, *args, **kwargs):
+        serializer = serializers.SubmitFederationTracksSerializer(
+            data=request.data)
+        serializer.is_valid(raise_exception=True)
+        batch = serializer.save(submitted_by=request.user)
+        for job in batch.jobs.all():
+            funkwhale_utils.on_commit(
+                tasks.import_job_run.delay,
+                import_job_id=job.pk,
+                use_acoustid=False,
+            )
+
+        return Response({'id': batch.id}, status=201)
+
     @transaction.atomic
     def _import_album(self, data, request, batch=None, import_request=None):
         # we import the whole album here to prevent race conditions that occurs
diff --git a/api/tests/music/test_views.py b/api/tests/music/test_views.py
index 468ea77e38f47848c1cf7090307d8cff8f5ffd86..f18d18c8615bd838f404a1a6024d93c639d017c8 100644
--- a/api/tests/music/test_views.py
+++ b/api/tests/music/test_views.py
@@ -1,6 +1,8 @@
 import io
 import pytest
 
+from django.urls import reverse
+
 from funkwhale_api.music import views
 from funkwhale_api.federation import actors
 
@@ -83,3 +85,21 @@ def test_can_proxy_remote_track(
     assert response.status_code == 200
     assert list(response.streaming_content) == [b't', b'e', b's', b't']
     assert response['Content-Type'] == track_file.library_track.audio_mimetype
+
+
+def test_can_create_import_from_federation_tracks(
+        factories, superuser_api_client, mocker):
+    lts = factories['federation.LibraryTrack'].create_batch(size=5)
+    mocker.patch('funkwhale_api.music.tasks.import_job_run')
+
+    payload = {
+        'library_tracks': [l.pk for l in lts]
+    }
+    url = reverse('api:v1:submit-federation')
+    response = superuser_api_client.post(url, payload)
+
+    assert response.status_code == 201
+    batch = superuser_api_client.user.imports.latest('id')
+    assert batch.jobs.count() == 5
+    for i, job in enumerate(batch.jobs.all()):
+        assert job.library_track == lts[i]
diff --git a/front/src/components/federation/LibraryTrackTable.vue b/front/src/components/federation/LibraryTrackTable.vue
new file mode 100644
index 0000000000000000000000000000000000000000..dc6eb9d210be0971b0b276b32a3675107ddda64a
--- /dev/null
+++ b/front/src/components/federation/LibraryTrackTable.vue
@@ -0,0 +1,143 @@
+<template>
+  <div>
+    <div class="ui inline form">
+      <input type="text" v-model="search" placeholder="Search by title, artist, domain..." />
+    </div>
+    <table v-if="result" class="ui compact very basic single line unstackable table">
+      <thead>
+        <tr>
+          <th colspan="1">
+            <div class="ui checkbox">
+              <input
+                type="checkbox"
+                @change="toggleCheckAll"
+                :checked="result.results.length === checked.length"><label>&nbsp;</label>
+            </div>
+          </th>
+          <th>Title</th>
+          <th>Artist</th>
+          <th>Album</th>
+          <th>Published date</th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr v-for="track in result.results">
+          <td class="collapsing">
+            <div v-if="!track.local_track_file" class="ui checkbox">
+              <input
+                type="checkbox"
+                @change="toggleCheck(track.id)"
+                :checked="checked.indexOf(track.id) > -1"><label>&nbsp;</label>
+            </div>
+            <div v-else class="ui label">
+              In library
+            </div>
+          </td>
+          <td>
+            {{ track.title }}
+          </td>
+          <td>
+            {{ track.artist_name }}
+          </td>
+          <td>
+            {{ track.album_title }}
+          </td>
+          <td>
+            <human-date :date="track.published_date"></human-date>
+          </td>
+        </tr>
+      </tbody>
+      <tfoot class="full-width">
+        <tr>
+          <th colspan="5">
+            <button
+              @click="launchImport"
+              :disabled="checked.length === 0 || isImporting"
+              :class="['ui', 'green', {loading: isImporting}, 'button']">Import {{ checked.length }} tracks
+            </button>
+          </th>
+        </tr>
+      </tfoot>
+    </table>
+  </div>
+</template>
+
+<script>
+import axios from 'axios'
+import _ from 'lodash'
+
+export default {
+  props: ['filters'],
+  data () {
+    return {
+      isLoading: false,
+      result: null,
+      page: 1,
+      paginateBy: 50,
+      search: '',
+      checked: {},
+      isImporting: false
+    }
+  },
+  created () {
+    this.fetchData()
+  },
+  methods: {
+    fetchData () {
+      let params = _.merge({
+        'page': this.page,
+        'paginate_by': this.paginateBy,
+        'q': this.search
+      }, this.filters)
+      let self = this
+      self.isLoading = true
+      self.checked = []
+      axios.get('/federation/library-tracks/', {params: params}).then((response) => {
+        self.result = response.data
+        self.isLoading = false
+      }, error => {
+        self.isLoading = false
+        self.errors = error.backendErrors
+      })
+    },
+    launchImport () {
+      let self = this
+      self.isImporting = true
+      let payload = {
+        library_tracks: this.checked
+      }
+      axios.post('/submit/federation/', payload).then((response) => {
+        console.log('Triggered import', response.data)
+        self.isImporting = false
+        self.fetchData()
+      }, error => {
+        self.isImporting = false
+        self.errors = error.backendErrors
+      })
+    },
+    toggleCheckAll () {
+      if (this.checked.length === this.result.results.length) {
+        // we uncheck
+        this.checked = []
+      } else {
+        this.checked = this.result.results.map(t => { return t.id })
+      }
+    },
+    toggleCheck (id) {
+      if (this.checked.indexOf(id) > -1) {
+        // we uncheck
+        this.checked.splice(this.checked.indexOf(id), 1)
+      } else {
+        this.checked.push(id)
+      }
+    }
+  },
+  watch: {
+    search (newValue) {
+      if (newValue.length > 0) {
+        this.fetchData()
+      }
+    }
+  }
+}
+</script>
diff --git a/front/src/views/federation/LibraryDetail.vue b/front/src/views/federation/LibraryDetail.vue
index d33fcc212829e411d7d51e9650437cc3ca4ddade..6d9ab2eeb87fbf567c4d787cc67a46c8323d90c4 100644
--- a/front/src/views/federation/LibraryDetail.vue
+++ b/front/src/views/federation/LibraryDetail.vue
@@ -82,6 +82,16 @@
                 <td>
                   <human-date v-if="object.fetched_date" :date="object.fetched_date"></human-date>
                   <template v-else>Never</template>
+                  <button
+                    @click="scan"
+                    v-if="!scanTrigerred"
+                    :class="['ui', 'basic', {loading: isScanLoading}, 'button']">
+                    <i class="sync icon"></i> Trigger scan
+                  </button>
+                  <button v-else class="ui success button">
+                    <i class="check icon"></i> Scan triggered!
+                  </button>
+
                 </td>
                 <td></td>
               </tr>
@@ -91,6 +101,7 @@
       </div>
       <div class="ui vertical stripe segment">
         <h2>Tracks available in this library</h2>
+        <library-track-table :filters="{library: id}"></library-track-table>
         <div class="ui stackable doubling three column grid">
         </div>
       </div>
@@ -102,13 +113,19 @@
 import axios from 'axios'
 import logger from '@/logging'
 
+import LibraryTrackTable from '@/components/federation/LibraryTrackTable'
+
 export default {
   props: ['id'],
-  components: {},
+  components: {
+    LibraryTrackTable
+  },
   data () {
     return {
       isLoading: true,
-      object: null
+      isScanLoading: false,
+      object: null,
+      scanTrigerred: false
     }
   },
   created () {
@@ -125,6 +142,18 @@ export default {
         self.isLoading = false
       })
     },
+    scan (until) {
+      var self = this
+      this.isScanLoading = true
+      let data = {}
+      let url = 'federation/libraries/' + this.id + '/scan/'
+      logger.default.debug('Triggering scan for library "' + this.id + '"')
+      axios.post(url, data).then((response) => {
+        self.scanTrigerred = true
+        logger.default.debug('Scan triggered with id', response.data)
+        self.isScanLoading = false
+      })
+    },
     update (attr) {
       let newValue = this.object[attr]
       let params = {}