diff --git a/changes/changelog.d/1060.feature b/changes/changelog.d/1060.feature new file mode 100644 index 0000000000000000000000000000000000000000..245a6be0ce9281b0771c8cb122f10b64c0cc5083 --- /dev/null +++ b/changes/changelog.d/1060.feature @@ -0,0 +1 @@ +Added a new radio based on another user listenings (#1060) diff --git a/front/src/App.vue b/front/src/App.vue index b572b1bcd0c4263375a205f43a2022b34140adbe..6252198e27e7f6d9b8f86d09d85f480b9d4d21b6 100644 --- a/front/src/App.vue +++ b/front/src/App.vue @@ -42,6 +42,7 @@ import { WebSocketBridge } from 'django-channels' import GlobalEvents from '@/components/utils/global-events' import moment from 'moment' import locales from './locales' +import {getClientOnlyRadio} from '@/radios' export default { name: 'app', @@ -138,6 +139,11 @@ export default { id: 'sidebarPendingReviewRequestCount', handler: this.incrementPendingReviewRequestsCountInSidebar }) + this.$store.commit('ui/addWebsocketEventHandler', { + eventName: 'Listen', + id: 'handleListen', + handler: this.handleListen + }) }, mounted () { let self = this @@ -175,6 +181,10 @@ export default { eventName: 'user_request.created', id: 'sidebarPendingReviewRequestCount', }) + this.$store.commit('ui/removeWebsocketEventHandler', { + eventName: 'Listen', + id: 'handleListen', + }) this.disconnect() }, methods: { @@ -190,6 +200,14 @@ export default { incrementPendingReviewRequestsCountInSidebar (event) { this.$store.commit('ui/incrementNotifications', {type: 'pendingReviewRequests', value: event.pending_count}) }, + handleListen (event) { + if (this.$store.state.radios.current && this.$store.state.radios.running) { + let current = this.$store.state.radios.current + if (current.clientOnly && current.type === 'account') { + getClientOnlyRadio(current).handleListen(current, event, this.$store) + } + } + }, async fetchNodeInfo () { let response = await axios.get('instance/nodeinfo/2.0/') this.$store.commit('instance/nodeinfo', response.data) diff --git a/front/src/components/audio/Player.vue b/front/src/components/audio/Player.vue index 58f76ad073b70a167811cb38f784c6b4fb367bba..1c720d9522b6c7258bd3f4b4a079f9079b066549 100644 --- a/front/src/components/audio/Player.vue +++ b/front/src/components/audio/Player.vue @@ -251,6 +251,8 @@ export default { progressInterval: null, maxPreloaded: 3, preloadDelay: 15, + listenDelay: 15, + listeningRecorded: null, soundsCache: [], soundId: null, playTimeout: null, @@ -477,6 +479,13 @@ export default { this.getSound(toPreload) this.nextTrackPreloaded = true } + if (t > this.listenDelay || d - t < 30) { + let onlyTrack = this.$store.state.queue.tracks.length === 1 + if (this.listeningRecorded != this.currentTrack) { + this.listeningRecorded = this.currentTrack + this.$store.dispatch('player/trackListened', this.currentTrack) + } + } } }, seek (step) { diff --git a/front/src/components/audio/track/Widget.vue b/front/src/components/audio/track/Widget.vue index 6895a8712730b2ad52d5951ae19fda4c788954e0..945db6a167f1e5ae5ef46ae5c102bf32b4cc6b49 100644 --- a/front/src/components/audio/track/Widget.vue +++ b/front/src/components/audio/track/Widget.vue @@ -29,7 +29,7 @@ <tags-list label-classes="tiny" :truncate-size="20" :limit="2" :show-more="false" :tags="object.track.tags"></tags-list> <div class="extra" v-if="isActivity"> - <span class="left floated">@{{ object.user.username }}</span> + <router-link class="left floated" :to="{name: 'profile.overview', params: {username: object.user.username}}">@{{ object.user.username }}</router-link> <span class="right floated"><human-date :date="object.creation_date" /></span> </div> </div> diff --git a/front/src/components/radios/Button.vue b/front/src/components/radios/Button.vue index f335a6596273838e844f1357f7c9c5503ace7ff7..7e1ff40c50467e3199ea2574955d4785c115da3b 100644 --- a/front/src/components/radios/Button.vue +++ b/front/src/components/radios/Button.vue @@ -8,10 +8,12 @@ <script> +import lodash from '@/lodash' export default { props: { customRadioId: {required: false}, type: {type: String, required: false}, + clientOnly: {type: Boolean, default: false}, objectId: {default: null} }, methods: { @@ -19,7 +21,12 @@ export default { if (this.running) { this.$store.dispatch('radios/stop') } else { - this.$store.dispatch('radios/start', {type: this.type, objectId: this.objectId, customRadioId: this.customRadioId}) + this.$store.dispatch('radios/start', { + type: this.type, + objectId: this.objectId, + customRadioId: this.customRadioId, + clientOnly: this.clientOnly, + }) } } }, @@ -30,7 +37,7 @@ export default { if (!state.running) { return false } else { - return current.type === this.type && current.objectId === this.objectId && current.customRadioId === this.customRadioId + return current.type === this.type && lodash.isEqual(current.objectId, this.objectId) && current.customRadioId === this.customRadioId } } } diff --git a/front/src/radios.js b/front/src/radios.js new file mode 100644 index 0000000000000000000000000000000000000000..b10937fb9f5607e04ed96936dfab9a02f65e0ed8 --- /dev/null +++ b/front/src/radios.js @@ -0,0 +1,51 @@ +import axios from "axios" +import logger from '@/logging' + +// import axios from 'axios' + +const RADIOS = { + // some radios are client side only, so we have to implement the populateQueue + // method by hand + account: { + offset: 1, + populateQueue({current, dispatch, playNow}) { + let params = {scope: `actor:${current.objectId.fullUsername}`, ordering: '-creation_date', page_size: 1, page: this.offset} + axios.get('history/listenings', {params}).then((response) => { + let latest = response.data.results[0] + if (!latest) { + logger.default.error('No more tracks') + dispatch('stop') + } + this.offset += 1 + let append = dispatch('queue/append', {track: latest.track}, {root: true}) + if (playNow) { + append.then(() => { + dispatch('queue/last', null, {root: true}) + }) + } + }, (error) => { + logger.default.error('Error while fetching listenings', error) + dispatch('stop') + }) + }, + stop () { + this.offset = 1 + }, + handleListen (current, event, store) { + // XXX: handle actors from other pods + if (event.actor.local_id === current.objectId.username) { + axios.get(`tracks/${event.object.local_id}`).then((response) => { + if (response.data.uploads.length > 0) { + store.dispatch('queue/append', {track: response.data, index: store.state.queue.currentIndex + 1}) + this.offset += 1 + } + }, (error) => { + logger.default.error('Cannot retrieve track info', error) + }) + } + } + } +} +export function getClientOnlyRadio({type}) { + return RADIOS[type] +} diff --git a/front/src/store/player.js b/front/src/store/player.js index 14affce90a6b56ea11693d6479991eb82c5848a0..f3dbbb8b79307aeb5a36918ba4f0e9345f93cd11 100644 --- a/front/src/store/player.js +++ b/front/src/store/player.js @@ -127,7 +127,6 @@ export default { }) }, trackEnded ({dispatch, rootState}, track) { - dispatch('trackListened', track) let queueState = rootState.queue if (queueState.currentIndex === queueState.tracks.length - 1) { // we've reached last track of queue, trigger a reload diff --git a/front/src/store/radios.js b/front/src/store/radios.js index 6eb06566ff21b7533c840e4c0c9974ae551ab8f3..d07ad59cbcf74405c958f44d6b75531c3d280b6d 100644 --- a/front/src/store/radios.js +++ b/front/src/store/radios.js @@ -1,6 +1,8 @@ import axios from 'axios' import logger from '@/logging' +import {getClientOnlyRadio} from '@/radios' + export default { namespaced: true, state: { @@ -42,11 +44,17 @@ export default { } }, actions: { - start ({commit, dispatch}, {type, objectId, customRadioId}) { + start ({commit, dispatch}, {type, objectId, customRadioId, clientOnly}) { var params = { radio_type: type, related_object_id: objectId, - custom_radio: customRadioId + custom_radio: customRadioId, + } + if (clientOnly) { + commit('current', {type, objectId, customRadioId, clientOnly}) + commit('running', true) + dispatch('populateQueue', true) + return } return axios.post('radios/sessions/', params).then((response) => { logger.default.info('Successfully started radio ', type) @@ -57,7 +65,10 @@ export default { logger.default.error('Error while starting radio', type) }) }, - stop ({commit}) { + stop ({commit, state}) { + if (state.current && state.current.clientOnly) { + getClientOnlyRadio(state.current).stop() + } commit('current', null) commit('running', false) }, @@ -71,6 +82,9 @@ export default { var params = { session: state.current.session } + if (state.current.clientOnly) { + return getClientOnlyRadio(state.current).populateQueue({current: state.current, dispatch, state, rootState, playNow}) + } return axios.post('radios/tracks/', params).then((response) => { logger.default.info('Adding track to queue from radio') let append = dispatch('queue/append', {track: response.data.track}, {root: true}) diff --git a/front/src/store/ui.js b/front/src/store/ui.js index 0c16f2b451b2e93f2c5e5973ffca80cbde409957..5c6ca20de4b142f5e2b75a9f8af6b98d8d54ebf3 100644 --- a/front/src/store/ui.js +++ b/front/src/store/ui.js @@ -31,6 +31,7 @@ export default { 'mutation.updated': {}, 'report.created': {}, 'user_request.created': {}, + 'Listen': {}, }, pageTitle: null, routePreferences: { diff --git a/front/src/views/auth/ProfileBase.vue b/front/src/views/auth/ProfileBase.vue index cb70b66ae788f3d14bec5d220076ba77a62655d0..702d3b870d572902f9328b45db96286402554d4d 100644 --- a/front/src/views/auth/ProfileBase.vue +++ b/front/src/views/auth/ProfileBase.vue @@ -43,6 +43,9 @@ </div> </template> </h1> + <div class="ui center aligned text"> + <radio-button type="account" :object-id="{username: object.preferred_username, fullUsername: object.full_username}" :client-only="true"></radio-button> + </div> <div class="ui small hidden divider"></div> <div v-if="$store.getters['ui/layoutVersion'] === 'large'"> <rendered-description @@ -82,6 +85,7 @@ import { mapState } from "vuex" import axios from 'axios' import ReportMixin from '@/components/mixins/Report' +import RadioButton from "@/components/radios/Button" export default { mixins: [ReportMixin], @@ -89,6 +93,9 @@ export default { username: {type: String, required: true}, domain: {type: String, required: false, default: null}, }, + components: { + RadioButton, + }, data () { return { object: null, diff --git a/front/tests/unit/specs/store/player.spec.js b/front/tests/unit/specs/store/player.spec.js index 13a3fd7630ab7974aa0a6534253ded81433e0003..c3f1c4583f39b28b7df07370dede94128415f3b3 100644 --- a/front/tests/unit/specs/store/player.spec.js +++ b/front/tests/unit/specs/store/player.spec.js @@ -136,7 +136,6 @@ describe('store/player', () => { payload: {test: 'track'}, params: {rootState: {queue: {currentIndex: 0, tracks: [1, 2]}}}, expectedActions: [ - { type: 'trackListened', payload: {test: 'track'} }, { type: 'queue/next', payload: null, options: {root: true} } ] }) @@ -147,7 +146,6 @@ describe('store/player', () => { payload: {test: 'track'}, params: {rootState: {queue: {currentIndex: 1, tracks: [1, 2]}}}, expectedActions: [ - { type: 'trackListened', payload: {test: 'track'} }, { type: 'radios/populateQueue', payload: null, options: {root: true} }, { type: 'queue/next', payload: null, options: {root: true} } ] diff --git a/front/tests/unit/specs/store/radios.spec.js b/front/tests/unit/specs/store/radios.spec.js index ff14f6145cd91a2326f01b3967bac82cb1f66797..512b0fc3f4ebb6d4b23703e6f8619ba81e34f479 100644 --- a/front/tests/unit/specs/store/radios.spec.js +++ b/front/tests/unit/specs/store/radios.spec.js @@ -58,6 +58,7 @@ describe('store/radios', () => { it('stop', () => { return testAction({ action: store.actions.stop, + params: {state: {}}, expectedMutations: [ { type: 'current', payload: null }, { type: 'running', payload: false }