diff --git a/front/src/components/library/Library.vue b/front/src/components/library/Library.vue
index f5303d88c8e14d8a959d541903e23e13ce100528..c27313dc36d2a9d08c0cbba2c0e704c7527c5063 100644
--- a/front/src/components/library/Library.vue
+++ b/front/src/components/library/Library.vue
@@ -3,6 +3,7 @@
     <div class="ui secondary pointing menu">
       <router-link class="ui item" to="/library" exact>Browse</router-link>
       <router-link class="ui item" to="/library/artists" exact>Artists</router-link>
+      <router-link class="ui item" to="/library/radios" exact>Radios</router-link>
       <div class="ui secondary right menu">
         <router-link v-if="$store.state.auth.availablePermissions['import.launch']" class="ui item" to="/library/import/launch" exact>Import</router-link>
         <router-link v-if="$store.state.auth.availablePermissions['import.launch']" class="ui item" to="/library/import/batches">Import batches</router-link>
diff --git a/front/src/components/library/Radios.vue b/front/src/components/library/Radios.vue
new file mode 100644
index 0000000000000000000000000000000000000000..409b6b6741137f6fef494cd42a11b33648498658
--- /dev/null
+++ b/front/src/components/library/Radios.vue
@@ -0,0 +1,164 @@
+<template>
+  <div>
+    <div class="ui vertical stripe segment">
+      <h2 class="ui header">Browsing radios</h2>
+      <router-link class="ui green basic button" to="/library/radios/build" exact>Create your own radio</router-link>
+      <div class="ui hidden divider"></div>
+      <div :class="['ui', {'loading': isLoading}, 'form']">
+        <div class="fields">
+          <div class="field">
+            <label>Search</label>
+            <input type="text" v-model="query" placeholder="Enter a radio name..."/>
+          </div>
+          <div class="field">
+            <label>Ordering</label>
+            <select class="ui dropdown" v-model="ordering">
+              <option v-for="option in orderingOptions" :value="option[0]">
+                {{ option[1] }}
+              </option>
+            </select>
+          </div>
+          <div class="field">
+            <label>Ordering direction</label>
+            <select class="ui dropdown" v-model="orderingDirection">
+              <option value="">Ascending</option>
+              <option value="-">Descending</option>
+            </select>
+          </div>
+          <div class="field">
+            <label>Results per page</label>
+            <select class="ui dropdown" v-model="paginateBy">
+              <option :value="parseInt(12)">12</option>
+              <option :value="parseInt(25)">25</option>
+              <option :value="parseInt(50)">50</option>
+            </select>
+          </div>
+        </div>
+      </div>
+      <div class="ui hidden divider"></div>
+      <div v-if="result" class="ui stackable three column grid">
+        <div
+          v-if="result.results.length > 0"
+          v-for="radio in result.results"
+          :key="radio.id"
+          class="column">
+          <radio-card class="fluid" type="custom" :custom-radio="radio"></radio-card>
+        </div>
+      </div>
+      <div class="ui center aligned basic segment">
+        <pagination
+          v-if="result && result.results.length > 0"
+          @page-changed="selectPage"
+          :current="page"
+          :paginate-by="paginateBy"
+          :total="result.count"
+          ></pagination>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import _ from 'lodash'
+import $ from 'jquery'
+
+import config from '@/config'
+import logger from '@/logging'
+
+import OrderingMixin from '@/components/mixins/Ordering'
+import PaginationMixin from '@/components/mixins/Pagination'
+import RadioCard from '@/components/radios/Card'
+import Pagination from '@/components/Pagination'
+
+const FETCH_URL = config.API_URL + 'radios/radios/'
+
+export default {
+  mixins: [OrderingMixin, PaginationMixin],
+  props: {
+    defaultQuery: {type: String, required: false, default: ''}
+  },
+  components: {
+    RadioCard,
+    Pagination
+  },
+  data () {
+    let defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date')
+    return {
+      isLoading: true,
+      result: null,
+      page: parseInt(this.defaultPage),
+      query: this.defaultQuery,
+      paginateBy: parseInt(this.defaultPaginateBy || 12),
+      orderingDirection: defaultOrdering.direction,
+      ordering: defaultOrdering.field,
+      orderingOptions: [
+        ['creation_date', 'Creation date'],
+        ['name', 'Name']
+      ]
+    }
+  },
+  created () {
+    this.fetchData()
+  },
+  mounted () {
+    $('.ui.dropdown').dropdown()
+  },
+  methods: {
+    updateQueryString: _.debounce(function () {
+      this.$router.replace({
+        query: {
+          query: this.query,
+          page: this.page,
+          paginateBy: this.paginateBy,
+          ordering: this.getOrderingAsString()
+        }
+      })
+    }, 500),
+    fetchData: _.debounce(function () {
+      var self = this
+      this.isLoading = true
+      let url = FETCH_URL
+      let params = {
+        page: this.page,
+        page_size: this.paginateBy,
+        name__icontains: this.query,
+        ordering: this.getOrderingAsString()
+      }
+      logger.default.debug('Fetching radios')
+      this.$http.get(url, {params: params}).then((response) => {
+        self.result = response.data
+        self.isLoading = false
+      })
+    }, 500),
+    selectPage: function (page) {
+      this.page = page
+    }
+  },
+  watch: {
+    page () {
+      this.updateQueryString()
+      this.fetchData()
+    },
+    paginateBy () {
+      this.updateQueryString()
+      this.fetchData()
+    },
+    ordering () {
+      this.updateQueryString()
+      this.fetchData()
+    },
+    orderingDirection () {
+      this.updateQueryString()
+      this.fetchData()
+    },
+    query () {
+      this.updateQueryString()
+      this.fetchData()
+    }
+  }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+</style>
diff --git a/front/src/components/library/import/FileUpload.vue b/front/src/components/library/import/FileUpload.vue
index 4681d79322727f9341682f5b8586bc0685001676..93ca75c3961d445f924bf920df7ef5e1da330ab3 100644
--- a/front/src/components/library/import/FileUpload.vue
+++ b/front/src/components/library/import/FileUpload.vue
@@ -93,18 +93,15 @@ export default {
     inputFile (newFile, oldFile) {
       if (newFile && !oldFile) {
         // add
-        console.log('add', newFile)
         if (!this.batch) {
           this.createBatch()
         }
       }
       if (newFile && oldFile) {
         // update
-        console.log('update', newFile)
       }
       if (!newFile && oldFile) {
         // remove
-        console.log('remove', oldFile)
       }
     },
     createBatch () {
diff --git a/front/src/components/library/radios/Builder.vue b/front/src/components/library/radios/Builder.vue
new file mode 100644
index 0000000000000000000000000000000000000000..f58d5003fd5b7ac21adac9b77142fa96206e0284
--- /dev/null
+++ b/front/src/components/library/radios/Builder.vue
@@ -0,0 +1,221 @@
+<template>
+  <div class="ui vertical stripe segment">
+    <div>
+      <div>
+        <h2 class="ui header">Builder</h2>
+        <p>
+          You can use this interface to build your own custom radio, which
+          will play tracks according to your criteria
+        </p>
+        <div class="ui form">
+          <div class="inline fields">
+            <div class="field">
+              <label for="name">Radio name</label>
+              <input id="name" type="text" v-model="radioName" placeholder="My awesome radio" />
+            </div>
+            <div class="field">
+              <input id="public" type="checkbox" v-model="isPublic" />
+              <label for="public">Display publicly</label>
+            </div>
+            <button :disabled="!canSave" @click="save" class="ui green button">Save</button>
+            <radio-button v-if="id" type="custom" :custom-radio-id="id"></radio-button>
+          </div>
+        </div>
+        <div class="ui form">
+          <p>Add filters to customize your radio</p>
+          <div class="inline field">
+            <select class="ui dropdown" v-model="currentFilterType">
+              <option value="">Select a filter</option>
+              <option v-for="f in availableFilters" :value="f.type">{{ f.label }}</option>
+            </select>
+            <button :disabled="!currentFilterType" @click="add" class="ui button">Add filter</button>
+          </div>
+          <p v-if="currentFilter">
+            {{ currentFilter.help_text }}
+          </p>
+        </div>
+        <table class="ui table">
+          <thead>
+            <tr>
+              <th class="two wide">Filter name</th>
+              <th class="one wide">Exclude</th>
+              <th class="six wide">Config</th>
+              <th class="five wide">Candidates</th>
+              <th class="two wide">Actions</th>
+            </tr>
+          </thead>
+          <tbody>
+            <builder-filter
+              v-for="(f, index) in filters"
+              :key="(f, index, f.hash)"
+              :index="index"
+              @update-config="updateConfig"
+              @delete="deleteFilter"
+              :config="f.config"
+              :filter="f.filter">
+            </builder-filter>
+          </tbody>
+        </table>
+        <template v-if="checkResult">
+          <h3 class="ui header">
+            {{ checkResult.candidates.count }} tracks matching combined filters
+          </h3>
+          <track-table v-if="checkResult.candidates.sample" :tracks="checkResult.candidates.sample"></track-table>
+        </template>
+      </div>
+    </div>
+  </div>
+</template>
+<script>
+import config from '@/config'
+import $ from 'jquery'
+import _ from 'lodash'
+import BuilderFilter from './Filter'
+import TrackTable from '@/components/audio/track/Table'
+import RadioButton from '@/components/radios/Button'
+
+export default {
+  props: {
+    id: {required: false}
+  },
+  components: {
+    BuilderFilter,
+    TrackTable,
+    RadioButton
+  },
+  data: function () {
+    return {
+      availableFilters: [],
+      currentFilterType: null,
+      filters: [],
+      checkResult: null,
+      radioName: '',
+      isPublic: true
+    }
+  },
+  created: function () {
+    let self = this
+    this.fetchFilters().then(() => {
+      if (self.id) {
+        self.fetch()
+      }
+    })
+  },
+  mounted () {
+    $('.ui.dropdown').dropdown()
+  },
+  methods: {
+    fetchFilters: function () {
+      let self = this
+      let url = config.API_URL + 'radios/radios/filters/'
+      return this.$http.get(url).then((response) => {
+        self.availableFilters = response.data
+      })
+    },
+    add () {
+      this.filters.push({
+        config: {},
+        filter: this.currentFilter,
+        hash: +new Date()
+      })
+      this.fetchCandidates()
+    },
+    updateConfig (index, field, value) {
+      this.filters[index].config[field] = value
+      this.fetchCandidates()
+    },
+    deleteFilter (index) {
+      this.filters.splice(index, 1)
+      this.fetchCandidates()
+    },
+    fetch: function () {
+      let self = this
+      let url = config.API_URL + 'radios/radios/' + this.id + '/'
+      this.$http.get(url).then((response) => {
+        self.filters = response.data.config.map(f => {
+          return {
+            config: f,
+            filter: this.availableFilters.filter(e => { return e.type === f.type })[0],
+            hash: +new Date()
+          }
+        })
+        self.radioName = response.data.name
+        self.isPublic = response.data.is_public
+      })
+    },
+    fetchCandidates: function () {
+      let self = this
+      let url = config.API_URL + 'radios/radios/validate/'
+      let final = this.filters.map(f => {
+        let c = _.clone(f.config)
+        c.type = f.filter.type
+        return c
+      })
+      final = {
+        'filters': [
+          {'type': 'group', filters: final}
+        ]
+      }
+      this.$http.post(url, final).then((response) => {
+        self.checkResult = response.data.filters[0]
+      })
+    },
+    save: function () {
+      let self = this
+      let final = this.filters.map(f => {
+        let c = _.clone(f.config)
+        c.type = f.filter.type
+        return c
+      })
+      final = {
+        'name': this.radioName,
+        'is_public': this.isPublic,
+        'config': final
+      }
+      if (this.id) {
+        let url = config.API_URL + 'radios/radios/' + this.id + '/'
+        this.$http.put(url, final).then((response) => {
+        })
+      } else {
+        let url = config.API_URL + 'radios/radios/'
+        this.$http.post(url, final).then((response) => {
+          self.$router.push({
+            name: 'library.radios.edit',
+            params: {
+              id: response.data.id
+            }
+          })
+        })
+      }
+    }
+  },
+  computed: {
+    canSave: function () {
+      return (
+        this.radioName.length > 0 && this.checkErrors.length === 0
+      )
+    },
+    checkErrors: function () {
+      if (!this.checkResult) {
+        return []
+      }
+      let errors = this.checkResult.errors
+      return errors
+    },
+    currentFilter: function () {
+      let self = this
+      return this.availableFilters.filter(e => {
+        return e.type === self.currentFilterType
+      })[0]
+    }
+  },
+  watch: {
+    filters: {
+      handler: function () {
+        this.fetchCandidates()
+      },
+      deep: true
+    }
+  }
+}
+</script>
diff --git a/front/src/components/library/radios/Filter.vue b/front/src/components/library/radios/Filter.vue
new file mode 100644
index 0000000000000000000000000000000000000000..dd170d8b3104da08e4fbd3b93091182fe4a3a2a4
--- /dev/null
+++ b/front/src/components/library/radios/Filter.vue
@@ -0,0 +1,150 @@
+<template>
+  <tr>
+    <td>{{ filter.label }}</td>
+    <td>
+      <div class="ui toggle checkbox">
+        <input name="public" type="checkbox" v-model="exclude" @change="$emit('update-config', index, 'not', exclude)">
+        <label></label>
+      </div>
+    </td>
+    <td>
+      <div
+        v-for="(f, index) in filter.fields"
+        class="ui field"
+        :key="(f.name, index)"
+        :ref="f.name">
+          <div :class="['ui', 'search', 'selection', 'dropdown', {'autocomplete': f.autocomplete}, {'multiple': f.type === 'list'}]">
+            <i class="dropdown icon"></i>
+            <div class="default text">{{ f.placeholder }}</div>
+            <input v-if="f.type === 'list' && config[f.name]" :value="config[f.name].join(',')" type="hidden">
+            <div v-if="config[f.name]" class="ui menu">
+              <div
+                v-if="f.type === 'list'"
+                v-for="(v, index) in config[f.name]"
+                class="ui item"
+                :data-value="v">
+                  <template v-if="config.names">
+                    {{ config.names[index] }}
+                  </template>
+                  <template v-else>{{ v }}</template>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+    </td>
+    <td>
+      <span
+        @click="showCandidadesModal = !showCandidadesModal"
+        v-if="checkResult"
+        :class="['ui', {'green': checkResult.candidates.count > 10}, 'label']">
+        {{ checkResult.candidates.count }} tracks matching filter
+      </span>
+      <modal v-if="checkResult" :show.sync="showCandidadesModal">
+        <div class="header">
+          Track matching filter
+        </div>
+        <div class="content">
+          <div class="description">
+            <track-table v-if="checkResult.candidates.count > 0" :tracks="checkResult.candidates.sample"></track-table>
+          </div>
+        </div>
+        <div class="actions">
+          <div class="ui black deny button">
+            Cancel
+          </div>
+        </div>
+      </modal>
+    </td>
+    <td>
+      <button @click="$emit('delete', index)" class="ui basic red button">Remove</button>
+    </td>
+  </tr>
+</template>
+<script>
+import config from '@/config'
+import $ from 'jquery'
+import _ from 'lodash'
+
+import Modal from '@/components/semantic/Modal'
+import TrackTable from '@/components/audio/track/Table'
+import BuilderFilter from './Filter'
+
+export default {
+  components: {
+    BuilderFilter,
+    TrackTable,
+    Modal
+  },
+  props: {
+    filter: {type: Object},
+    config: {type: Object},
+    index: {type: Number}
+  },
+  data: function () {
+    return {
+      checkResult: null,
+      showCandidadesModal: false,
+      exclude: config.not
+    }
+  },
+  mounted: function () {
+    let self = this
+    this.filter.fields.forEach(f => {
+      let selector = ['.dropdown']
+      let settings = {
+        onChange: function (value, text, $choice) {
+          value = $(this).dropdown('get value').split(',')
+          if (f.type === 'list' && f.subtype === 'number') {
+            value = value.map(e => {
+              return parseInt(e)
+            })
+          }
+          self.value = value
+          self.$emit('update-config', self.index, f.name, value)
+          self.fetchCandidates()
+        }
+      }
+      if (f.type === 'list') {
+        selector.push('.multiple')
+      }
+      if (f.autocomplete) {
+        selector.push('.autocomplete')
+        settings.fields = f.autocomplete_fields
+        settings.minCharacters = 1
+        settings.apiSettings = {
+          url: config.BACKEND_URL + f.autocomplete + '?' + f.autocomplete_qs,
+          beforeXHR: function (xhrObject) {
+            xhrObject.setRequestHeader('Authorization', self.$store.getters['auth/header'])
+            return xhrObject
+          },
+          onResponse: function (initialResponse) {
+            if (settings.fields.remoteValues) {
+              return initialResponse
+            }
+            return {results: initialResponse}
+          }
+        }
+      }
+      $(self.$el).find(selector.join('')).dropdown(settings)
+    })
+  },
+  methods: {
+    fetchCandidates: function () {
+      let self = this
+      let url = config.API_URL + 'radios/radios/validate/'
+      let final = _.clone(this.config)
+      final.type = this.filter.type
+      final = {'filters': [final]}
+      this.$http.post(url, final).then((response) => {
+        self.checkResult = response.data.filters[0]
+      })
+    }
+  },
+  watch: {
+    exclude: function () {
+      this.fetchCandidates()
+    }
+  }
+}
+</script>
diff --git a/front/src/components/radios/Button.vue b/front/src/components/radios/Button.vue
index 4bf4279890d05f39ac06a350164b9c2101747068..819aa8651f3509827b0460c54c9cec9ded18d23b 100644
--- a/front/src/components/radios/Button.vue
+++ b/front/src/components/radios/Button.vue
@@ -11,7 +11,8 @@
 
 export default {
   props: {
-    type: {type: String, required: true},
+    customRadioId: {required: false},
+    type: {type: String, required: false},
     objectId: {type: Number, default: null}
   },
   methods: {
@@ -19,7 +20,7 @@ export default {
       if (this.running) {
         this.$store.dispatch('radios/stop')
       } else {
-        this.$store.dispatch('radios/start', {type: this.type, objectId: this.objectId})
+        this.$store.dispatch('radios/start', {type: this.type, objectId: this.objectId, customRadioId: this.customRadioId})
       }
     }
   },
diff --git a/front/src/components/radios/Card.vue b/front/src/components/radios/Card.vue
index dc8a24ff3c2e31d901ee11fb27eceb976ca7e819..d2c14c37c78dfbc23b858c93a5eddd28b3519fb9 100644
--- a/front/src/components/radios/Card.vue
+++ b/front/src/components/radios/Card.vue
@@ -1,13 +1,19 @@
 <template>
     <div class="ui card">
       <div class="content">
-        <div class="header">Radio : {{ radio.name }}</div>
+        <div class="header">{{ radio.name }}</div>
         <div class="description">
           {{ radio.description }}
         </div>
       </div>
       <div class="extra content">
-        <radio-button class="right floated button" :type="type"></radio-button>
+        <router-link
+          class="ui basic yellow button"
+          v-if="$store.state.auth.authenticated && type === 'custom' && customRadio.user === $store.state.auth.profile.id"
+          :to="{name: 'library.radios.edit', params: {id: customRadioId }}">
+          Edit...
+        </router-link>
+        <radio-button class="right floated button" :type="type" :custom-radio-id="customRadioId"></radio-button>
       </div>
     </div>
 </template>
@@ -17,14 +23,24 @@ import RadioButton from './Button'
 
 export default {
   props: {
-    type: {type: String, required: true}
+    type: {type: String, required: true},
+    customRadio: {required: false}
   },
   components: {
     RadioButton
   },
   computed: {
     radio () {
+      if (this.customRadio) {
+        return this.customRadio
+      }
       return this.$store.getters['radios/types'][this.type]
+    },
+    customRadioId: function () {
+      if (this.customRadio) {
+        return this.customRadio.id
+      }
+      return null
     }
   }
 }
diff --git a/front/src/router/index.js b/front/src/router/index.js
index f4efc723f4abc2fb9dfdca2e062d433c09d4e91e..971ef05cd82f034b9513716a42f35b02a5291054 100644
--- a/front/src/router/index.js
+++ b/front/src/router/index.js
@@ -13,6 +13,8 @@ import LibraryArtists from '@/components/library/Artists'
 import LibraryAlbum from '@/components/library/Album'
 import LibraryTrack from '@/components/library/Track'
 import LibraryImport from '@/components/library/import/Main'
+import LibraryRadios from '@/components/library/Radios'
+import RadioBuilder from '@/components/library/radios/Builder'
 import BatchList from '@/components/library/import/BatchList'
 import BatchDetail from '@/components/library/import/BatchDetail'
 
@@ -76,6 +78,19 @@ export default new Router({
             defaultPage: route.query.page
           })
         },
+        {
+          path: 'radios/',
+          name: 'library.radios.browse',
+          component: LibraryRadios,
+          props: (route) => ({
+            defaultOrdering: route.query.ordering,
+            defaultQuery: route.query.query,
+            defaultPaginateBy: route.query.paginateBy,
+            defaultPage: route.query.page
+          })
+        },
+        { path: 'radios/build', name: 'library.radios.build', component: RadioBuilder, props: true },
+        { path: 'radios/build/:id', name: 'library.radios.edit', component: RadioBuilder, props: true },
         { path: 'artists/:id', name: 'library.artists.detail', component: LibraryArtist, props: true },
         { path: 'albums/:id', name: 'library.albums.detail', component: LibraryAlbum, props: true },
         { path: 'tracks/:id', name: 'library.tracks.detail', component: LibraryTrack, props: true },
diff --git a/front/src/store/radios.js b/front/src/store/radios.js
index a9c429876a4ff974635de9f73062dd2597237209..600b24b31e7fb77eafdda8a0992bdf58879f3a54 100644
--- a/front/src/store/radios.js
+++ b/front/src/store/radios.js
@@ -38,15 +38,16 @@ export default {
     }
   },
   actions: {
-    start ({commit, dispatch}, {type, objectId}) {
+    start ({commit, dispatch}, {type, objectId, customRadioId}) {
       let resource = Vue.resource(CREATE_RADIO_URL)
       var params = {
         radio_type: type,
-        related_object_id: objectId
+        related_object_id: objectId,
+        custom_radio: customRadioId
       }
       resource.save({}, params).then((response) => {
         logger.default.info('Successfully started radio ', type)
-        commit('current', {type, objectId, session: response.data.id})
+        commit('current', {type, objectId, session: response.data.id, customRadioId})
         commit('running', true)
         dispatch('populateQueue')
       }, (response) => {