diff --git a/api/pytest.ini b/api/pytest.ini index 4ab907403097c796663f02d6a878ccd197970a23..9be63d3531626526f46f196b840c02a9cae1c8b3 100644 --- a/api/pytest.ini +++ b/api/pytest.ini @@ -3,3 +3,4 @@ DJANGO_SETTINGS_MODULE=config.settings.test # -- recommended but optional: python_files = tests.py test_*.py *_tests.py +testpatsh = tests diff --git a/front/package.json b/front/package.json index 5bec01602d4beed1e1443af3fd690bdf86e06e00..3eb5201b29d1a6cc76212a290459c471b23a6677 100644 --- a/front/package.json +++ b/front/package.json @@ -23,7 +23,8 @@ "vue-resource": "^1.3.4", "vue-router": "^2.3.1", "vuedraggable": "^2.14.1", - "vuex": "^3.0.1" + "vuex": "^3.0.1", + "vuex-persistedstate": "^2.4.2" }, "devDependencies": { "autoprefixer": "^6.7.2", diff --git a/front/src/cache/index.js b/front/src/cache/index.js deleted file mode 100644 index e039ee788078daa4e11e1df194982e33014240af..0000000000000000000000000000000000000000 --- a/front/src/cache/index.js +++ /dev/null @@ -1,29 +0,0 @@ -import logger from '@/logging' -export default { - get (key, d) { - let v = localStorage.getItem(key) - if (v === null) { - return d - } else { - try { - return JSON.parse(v).value - } catch (e) { - logger.default.error('Removing unparsable cached value for key ' + key) - this.remove(key) - return d - } - } - }, - set (key, value) { - return localStorage.setItem(key, JSON.stringify({value: value})) - }, - - remove (key) { - return localStorage.removeItem(key) - }, - - clear () { - localStorage.clear() - } - -} diff --git a/front/src/components/Sidebar.vue b/front/src/components/Sidebar.vue index d6a2539224afb89412f5488cd1e7b02ade452cdb..a315aab199c341a24db3baed09cdad32a813975b 100644 --- a/front/src/components/Sidebar.vue +++ b/front/src/components/Sidebar.vue @@ -62,7 +62,7 @@ {{ track.artist.name }} </td> <td> - <template v-if="favoriteTracks.objects[track.id]"> + <template v-if="$store.getters['favorites/isFavorite'](track.id)"> <i class="pink heart icon"></i> </template </td> @@ -94,7 +94,6 @@ import {mapState, mapActions} from 'vuex' import Player from '@/components/audio/Player' -import favoriteTracks from '@/favorites/tracks' import Logo from '@/components/Logo' import SearchBar from '@/components/audio/SearchBar' import backend from '@/audio/backend' @@ -112,8 +111,7 @@ export default { }, data () { return { - backend: backend, - favoriteTracks + backend: backend } }, mounted () { diff --git a/front/src/components/audio/Player.vue b/front/src/components/audio/Player.vue index fec74b3dcb26411d6cfedf4125c32d4a810ce574..500f4dc1d2b09e26cf036b871f842ca608b81d3b 100644 --- a/front/src/components/audio/Player.vue +++ b/front/src/components/audio/Player.vue @@ -5,6 +5,8 @@ v-if="currentTrack" :key="(currentIndex, currentTrack.id)" :is-current="true" + :start-time="$store.state.player.currentTime" + :autoplay="$store.state.player.playing" :track="currentTrack"> </audio-track> @@ -127,7 +129,7 @@ @keydown.ctrl.right.prevent.exact="next" @keydown.ctrl.down.prevent.exact="$store.commit('player/incrementVolume', -0.1)" @keydown.ctrl.up.prevent.exact="$store.commit('player/incrementVolume', 0.1)" - @keydown.f.prevent.exact="favoriteTracks.toggle(currentTrack.id)" + @keydown.f.prevent.exact="$store.dispatch('favorites/toggle', currentTrack.id)" @keydown.l.prevent.exact="$store.commit('player/toggleLooping')" @keydown.s.prevent.exact="shuffle" /> @@ -139,7 +141,6 @@ import {mapState, mapGetters, mapActions} from 'vuex' import GlobalEvents from '@/components/utils/global-events' -import favoriteTracks from '@/favorites/tracks' import Track from '@/audio/track' import AudioTrack from '@/components/audio/Track' import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon' @@ -154,8 +155,7 @@ export default { data () { return { sliderVolume: this.volume, - Track: Track, - favoriteTracks + Track: Track } }, mounted () { diff --git a/front/src/components/audio/SearchBar.vue b/front/src/components/audio/SearchBar.vue index 386e24a74f677d40fc60d1b536c15793288a5454..9d8b39f870cac19021fc56d9df3368d8f05f6ddf 100644 --- a/front/src/components/audio/SearchBar.vue +++ b/front/src/components/audio/SearchBar.vue @@ -18,6 +18,7 @@ const SEARCH_URL = config.API_URL + 'search?query={query}' export default { mounted () { + let self = this jQuery(this.$el).search({ type: 'category', minCharacters: 3, @@ -26,7 +27,7 @@ export default { }, apiSettings: { beforeXHR: function (xhrObject) { - xhrObject.setRequestHeader('Authorization', this.$store.getters['auth/header']) + xhrObject.setRequestHeader('Authorization', self.$store.getters['auth/header']) return xhrObject }, onResponse: function (initialResponse) { diff --git a/front/src/components/audio/Track.vue b/front/src/components/audio/Track.vue index f0e1f14fa70fbb52a85ba5d99ddc7ffdb4e11dbf..c8627925ebdd404e4d8dc9887d8a0307c22705fd 100644 --- a/front/src/components/audio/Track.vue +++ b/front/src/components/audio/Track.vue @@ -22,7 +22,9 @@ import url from '@/utils/url' export default { props: { track: {type: Object}, - isCurrent: {type: Boolean, default: false} + isCurrent: {type: Boolean, default: false}, + startTime: {type: Number, default: 0}, + autoplay: {type: Boolean, default: false} }, computed: { ...mapState({ @@ -57,8 +59,11 @@ export default { }, loaded: function () { - this.$store.commit('player/duration', this.$refs.audio.duration) - if (this.isCurrent) { + if (this.isCurrent && this.autoplay) { + this.$store.commit('player/duration', this.$refs.audio.duration) + if (this.startTime) { + this.setCurrentTime(this.startTime) + } this.$store.commit('player/playing', true) this.$refs.audio.play() } @@ -72,8 +77,9 @@ export default { if (this.looping === 1) { this.setCurrentTime(0) this.$refs.audio.play() + } else { + this.$store.dispatch('player/trackEnded', this.track) } - this.$store.dispatch('player/trackEnded', this.track) }, setCurrentTime (t) { if (t < 0 | t > this.duration) { diff --git a/front/src/components/favorites/List.vue b/front/src/components/favorites/List.vue index aef4bea93c1a0a6bfdbd695098619f1d45d6af22..8577e84ca5d339e7ed3ad0b10a4e99ef69411c83 100644 --- a/front/src/components/favorites/List.vue +++ b/front/src/components/favorites/List.vue @@ -119,7 +119,6 @@ export default { self.results = response.data self.nextLink = response.data.next self.previousLink = response.data.previous - self.$store.commit('favorites/count', response.data.count) self.results.results.forEach((track) => { self.$store.commit('favorites/track', {id: track.id, value: true}) }) diff --git a/front/src/components/favorites/TrackFavoriteIcon.vue b/front/src/components/favorites/TrackFavoriteIcon.vue index 5abc57a952446b6b9d34e366f09c8af7e617dcd0..d4838ba5f336ff0375145ed5925029a19bf68587 100644 --- a/front/src/components/favorites/TrackFavoriteIcon.vue +++ b/front/src/components/favorites/TrackFavoriteIcon.vue @@ -1,5 +1,5 @@ <template> - <button @click="$store.dispatch('favorites/set', {id: track.id, value: !isFavorite})" v-if="button" :class="['ui', 'pink', {'inverted': isFavorite}, {'favorited': isFavorite}, 'button']"> + <button @click="$store.dispatch('favorites/toggle', track.id)" v-if="button" :class="['ui', 'pink', {'inverted': isFavorite}, {'favorited': isFavorite}, 'button']"> <i class="heart icon"></i> <template v-if="isFavorite"> In favorites @@ -8,23 +8,16 @@ Add to favorites </template> </button> - <i v-else @click="$store.dispatch('favorites/set', {id: track.id, value: !isFavorite})" :class="['favorite-icon', 'heart', {'pink': isFavorite}, {'favorited': isFavorite}, 'link', 'icon']" :title="title"></i> + <i v-else @click="$store.dispatch('favorites/toggle', track.id)" :class="['favorite-icon', 'heart', {'pink': isFavorite}, {'favorited': isFavorite}, 'link', 'icon']" :title="title"></i> </template> <script> -import {mapState} from 'vuex' - export default { props: { track: {type: Object}, button: {type: Boolean, default: false} }, computed: { - ...mapState({ - favorites: state => { - return state.favorites.tracks - } - }), title () { if (this.isFavorite) { return 'Remove from favorites' diff --git a/front/src/components/metadata/Search.vue b/front/src/components/metadata/Search.vue index c3dc7433c5e268baa4e848bcb9a22125b555bd93..f2dea6cab9dfa45c0edc8fb3ff995961ca1b3dea 100644 --- a/front/src/components/metadata/Search.vue +++ b/front/src/components/metadata/Search.vue @@ -65,7 +65,7 @@ export default { }, apiSettings: { beforeXHR: function (xhrObject, s) { - xhrObject.setRequestHeader('Authorization', this.$store.getters['auth/header']) + xhrObject.setRequestHeader('Authorization', self.$store.getters['auth/header']) return xhrObject }, onResponse: function (initialResponse) { diff --git a/front/src/store/auth.js b/front/src/store/auth.js index d4b23adcb03401d75721310b22383460d64fe113..eea508df9518b66c050750d8d654748a7aee9df8 100644 --- a/front/src/store/auth.js +++ b/front/src/store/auth.js @@ -1,9 +1,7 @@ import Vue from 'vue' import config from '@/config' import logger from '@/logging' -import cache from '@/cache' import router from '@/router' -// import favoriteTracks from '@/favorites/tracks' const LOGIN_URL = config.API_URL + 'token/' const USER_PROFILE_URL = config.API_URL + 'users/users/me/' @@ -46,9 +44,7 @@ export default { return resource.save({}, credentials).then(response => { logger.default.info('Successfully logged in as', credentials.username) commit('token', response.data.token) - cache.set('token', response.data.token) commit('username', credentials.username) - cache.set('username', credentials.username) commit('authenticated', true) dispatch('fetchProfile') // Redirect to a specified route @@ -59,7 +55,6 @@ export default { }) }, logout ({commit}) { - cache.clear() commit('authenticated', false) commit('profile', null) logger.default.info('Log out, goodbye!') @@ -67,8 +62,8 @@ export default { }, check ({commit, dispatch, state}) { logger.default.info('Checking authentication...') - var jwt = cache.get('token') - var username = cache.get('username') + var jwt = state.token + var username = state.username if (jwt) { commit('authenticated', true) commit('username', username) diff --git a/front/src/store/favorites.js b/front/src/store/favorites.js index 8bb4bb5afe6add992547b631a91c3683e00fbb6d..9337966fdf68bc84202a8d2fe3e7add193d1fa20 100644 --- a/front/src/store/favorites.js +++ b/front/src/store/favorites.js @@ -14,16 +14,16 @@ export default { mutations: { track: (state, {id, value}) => { if (value) { - state.tracks.push(id) + if (state.tracks.indexOf(id) === -1) { + state.tracks.push(id) + } } else { let i = state.tracks.indexOf(id) if (i > -1) { state.tracks.splice(i, 1) } } - }, - count: (state, value) => { - state.count = value + state.count = state.tracks.length } }, getters: { @@ -35,29 +35,25 @@ export default { set ({commit, state}, {id, value}) { commit('track', {id, value}) if (value) { - commit('count', state.count + 1) let resource = Vue.resource(FAVORITES_URL) resource.save({}, {'track': id}).then((response) => { logger.default.info('Successfully added track to favorites') }, (response) => { logger.default.info('Error while adding track to favorites') commit('track', {id, value: !value}) - commit('count', state.count - 1) }) } else { - commit('count', state.count - 1) let resource = Vue.resource(REMOVE_URL) resource.delete({}, {'track': id}).then((response) => { logger.default.info('Successfully removed track from favorites') }, (response) => { logger.default.info('Error while removing track from favorites') commit('track', {id, value: !value}) - commit('count', state.count + 1) }) } }, toggle ({getters, dispatch}, id) { - dispatch('set', {id, value: getters['isFavorite'](id)}) + dispatch('set', {id, value: !getters['isFavorite'](id)}) }, fetch ({dispatch, state, commit}, url) { // will fetch favorites by batches from API to have them locally @@ -68,7 +64,6 @@ export default { response.data.results.forEach(result => { commit('track', {id: result.track, value: true}) }) - commit('count', state.tracks.length) if (response.data.next) { dispatch('fetch', response.data.next) } diff --git a/front/src/store/index.js b/front/src/store/index.js index 99e466e510f0cad3de2abfdaad4847bd0083defd..507f0b5876772364185fb6654e0b0a25fe95c913 100644 --- a/front/src/store/index.js +++ b/front/src/store/index.js @@ -1,5 +1,6 @@ import Vue from 'vue' import Vuex from 'vuex' +import createPersistedState from 'vuex-persistedstate' import favorites from './favorites' import auth from './auth' @@ -16,5 +17,84 @@ export default new Vuex.Store({ queue, radios, player - } + }, + plugins: [ + createPersistedState({ + key: 'auth', + paths: ['auth'], + filter: (mutation) => { + return mutation.type.startsWith('auth/') + } + }), + createPersistedState({ + key: 'radios', + paths: ['radios'], + filter: (mutation) => { + return mutation.type.startsWith('radios/') + } + }), + createPersistedState({ + key: 'player', + paths: [ + 'player.looping', + 'player.playing', + 'player.volume', + 'player.duration', + 'player.errored'], + filter: (mutation) => { + return mutation.type.startsWith('player/') && mutation.type !== 'player/currentTime' + } + }), + createPersistedState({ + key: 'progress', + paths: ['player.currentTime'], + filter: (mutation) => { + let delay = 10 + return mutation.type === 'player/currentTime' && parseInt(mutation.payload) % delay === 0 + }, + reducer: (state) => { + return { + player: { + currentTime: state.player.currentTime + } + } + } + }), + createPersistedState({ + key: 'queue', + filter: (mutation) => { + return mutation.type.startsWith('queue/') + }, + reducer: (state) => { + return { + queue: { + currentIndex: state.queue.currentIndex, + tracks: state.queue.tracks.map(track => { + // we keep only valuable fields to make the cache lighter and avoid + // cyclic value serialization errors + let artist = { + id: track.artist.id, + mbid: track.artist.mbid, + name: track.artist.name + } + return { + id: track.id, + title: track.title, + mbid: track.mbid, + album: { + id: track.album.id, + title: track.album.title, + mbid: track.album.mbid, + cover: track.album.cover, + artist: artist + }, + artist: artist, + files: track.files + } + }) + } + } + } + }) + ] }) diff --git a/front/src/store/queue.js b/front/src/store/queue.js index 3a0b7dd7979471f647dd5743602fb1b8fd5ea0bd..5dde19bd8e6f1665a826b522820e37ae3c14efaf 100644 --- a/front/src/store/queue.js +++ b/front/src/store/queue.js @@ -111,11 +111,6 @@ export default { } }, next ({state, dispatch, commit, rootState}) { - if (rootState.player.looping === 1) { - // we loop on the same track, this is handled directly on the track - // component, so we do nothing. - return logger.default.info('Looping on the same track') - } if (rootState.player.looping === 2 && state.currentIndex >= state.tracks.length - 1) { logger.default.info('Going back to the beginning of the queue') return dispatch('currentIndex', 0) @@ -130,6 +125,8 @@ export default { }, currentIndex ({commit, state, rootState, dispatch}, index) { commit('ended', false) + commit('player/currentTime', 0, {root: true}) + commit('player/playing', true, {root: true}) commit('player/errored', false, {root: true}) commit('currentIndex', index) if (state.tracks.length - index <= 2 && rootState.radios.running) {