Creating a Spotify-like streaming service with
Django and Vue

Lessons learned

By Emmanuelle Delescolle

Who am I?

Vue, are you sure you didn't mean Ember?

Getting started

.
├── docs
│   └── img
├── front
│   ├── public
│   ├── src
│   └── tests
└── radioify
      ├── management
      ├── migrations
      ├── static
      └── tests

Directory structure

Getting started

.
├── docs
│   └── img
├── front
│   ├── public
│   ├── src
│   └── tests
└── radioify
      ├── management
      ├── migrations
      ├── static
      └── tests

Starting your Vue project

npm install -g @vue/cli
vue create front

Getting both development servers to play nicely together

module.exports = {
  devServer: {
    proxy: {
      '/api/*': {
        target: 'http://localhost:8000/',
      },
      '/ws/*': {
        target: 'http://localhost:8000/',
        ws: true,
      },
    },
  },
};

front/vue.config.js

From Vue to Django

Getting both development servers to play nicely together

...
urlpatterns += [                                             
    re_path(r'^(?P<url>.*)$', VueView.as_view()),            
]

urls.py

From Django to Vue

...
class VueView(RedirectView):
                                             
    url = "http://localhost:8080/%(url)s" 

radioify/views.py

Vue tools

Browser Extension

Vue tools

CheatSheet

Vue tools

Vue CLI

create

inspect

add

build

serve

Vue Basics

  • router.js == urls.py
  • App.vue ~= templates/base.html
  • Components == Inclusion TemplateTag

Vue Basics

router.js - router/index.js

Vue.use(VueRouter);

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home,
  }, {
    path: '/artists/:id',
    name: 'Artists',
    component: ArtistDetail,
  }, 
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes,
});

export default router;

front/src/router/index.js

Similar to urls.py

  • path
  • name
  • component (view function)

Vue Basics

App.vue

.vue ???

  • HTML -> <template/>
  • JS -> <script/>
  • (S)CSS -> <style/>

Vue Basics

App.vue

<template src="./App.html" />
<script src="./App.js" />
<style src="./style/custom.scss" lang="scss"/>

front/src/App.vue

<div id="app" class="d-flex h-100">
    <transition name="fade" mode="out-in">
      <router-view
        id="main"
        :key="$route.name + ($route.params.id || '') + ($route.params.query || '')" 
      />
  </transition>
</div>

front/src/App.vue

Vue Basics

Component - .js

import YTResult from '@/models/YTResult';
import router from '@/router';

export default {
  name: 'YTButton',
  components: {
  },
  props: {
    itemId: String,
    itemTitle: String,
  },
  data() {
    return {
      isBusy: false,
    };
  },

front/src/components/YTButton/component.js

  methods: {
    download() {
      this.isBusy = true;
      YTResult.download(this.itemId).then((res) => {
        const { data } = res.response;
        router.push({ name: 'Songs', params: { id: data.song_id } });
      }).catch((err) => {
        this.$bvToast.toast(`while downloading ${this.itemTitle}`, {
          title: 'Something went wrong',
          variant: 'danger',
        });
      }).finally(() => {
        this.isBusy = false;
      });
    },
  },
  computed: {
    icon() {
      return this.isBusy ? 'spinner' : 'cloud-download-alt';
    },
    iconClass() {
      return this.isBusy ? '' : 'text-info';
    },
  },
};

Vue Basics

Component - .html

<button :disabled="isBusy" @click="download">
  <font-awesome-icon :icon="icon" :spin="isBusy" :pulse="isBusy" :class="iconClass"/>
</button>

front/src/components/YTButton/template.html

Data Exchange

Django - DRF

class ArtistViewSet(ModelViewSet):
    queryset = Artist.objects.all()
    serializer_class ArtistSerializer

radioify/viewsets.py

class ArtistSerializer(ModelSerializer):
    class Meta:
      model = Artist
      fields = ('id', '__str__', 'name', 'songs')

radioify/serializers.py

Data Exchange

Django - DRF + DRF_Schema_Adapter

@register
class ArtistEndpoint(Endpoint):
    model = Artist
    extra_fields = ('songs', )
    search_fields = ('name', )
    filter_fields = ('songs__id', )

radioify/endpoints.py

Data Exchange

Vue - VueX + VueXORM + Axios

const Axios = axios.create({
  baseURL: config.api_base_url,
  headers: {
    Accept: 'application/json',
    'Content-Type': 'application/json',
  },
  secure: false,
  withCredentials: false,
  xsrfCookieName: 'csrftoken',
  xsrfHeaderName: 'X-CSRFTOKEN',
});
Vue.prototype.$http = Axios;

front/src/store/index.js

Axios

Data Exchange

Vue - VueX + VueXORM + Axios

VuexORM.use(VuexORMAxios, {
  axios: Axios,
});
VuexORM.use(VuexORMisDirtyPlugin);
const database = new VuexORM.Database();

database.register(Artist);

front/src/store/index.js

VueXORM

Data Exchange

Vue - VueX + VueXORM + Axios

Vue.use(Vuex);

const store = new Vuex.Store({
  plugins: [VuexORM.install(database)],
});

export default store;

front/src/store/index.js

VueX

Data Exchange

Vue - VueX + VueXORM + Axios

import Song from '@/models/Song';
import BaseModel from './Base';

export default class Artist extends BaseModel {
  static entity = 'radioify/artists';

  static fields() {
    return {
      id: this.attr(null),
      name: this.string(''),
      image: this.string(''),
      bio: this.attr(),
      info: this.attr(),
      songs: this.hasMany(Song, 'artist'),
    };
  }
}

front/src/models/Artist.js

Models

Data Exchange

Vue - VueX + VueXORM + Axios

import { Model } from '@vuex-orm/core';

export default class BaseModel extends Model {
  static fetchAll() {
    return this.api().get(`${this.entity}/`, { dataKey: 'results' });
  }
  static fetch(id) {
    return this.api().get(`${this.entity}/${id}/`);
  }
  static filter(params) {
    return this.api().get(`${this.entity}/`, { params, dataKey: 'results' });
  }
}

front/src/models/Base.js

Models

Data Exchange

Vue - VueX + VueXORM + Axios

front/src/components/ArtistDetail/template.html

Accessing Data in the frontend

Caveat

 <ul class="song-list">
  <li v-for="song in artist.songs" :index="song.id">
    <a title="Direct play" @click="playNow(song.file)">
      <font-awesome-icon icon="play" class="text-success" />
    </a>
  </li>
</ul>

front/src/components/ArtistDetail/component.js

computed: {
  artist() {
    return Artist.query().with(['songs']).where('id', parseInt(this.$route.params.id, 10)).first();
  },
},

Websockets

Django - Channels

routing.py

from channels.auth import AuthMiddlewareStack                  
from channels.routing import ProtocolTypeRouter, URLRouter     
import radioify.routing                                    
                                                       
application = ProtocolTypeRouter({                             
    'websocket': AuthMiddlewareStack(                          
        URLRouter(                                             
            radioify.routing.websocket_urlpatterns             
        )                                                      
    )                                 
}) 

radioify/routing.py

from django.urls import re_path                                
from . import consumers                                        
                                 
websocket_urlpatterns = [                              
    re_path(r'ws/queue/(?P<username>\w+)/$', consumers.QueueConsumer),
]

Websockets

Vue - VueNativeSocket

front/src/main.js

Vue.use(VueNativeSock, `${protocol}://${window.location.host}${config.websocket_base_url}/queue/`,
  connectManually: true,
  format: 'json',
  reconnection: true,
  reconnectionAttempts: 5,
  reconnectionDelay: 30000,
  store,
});

front/src/store/index.js

const store = new Vuex.Store({
  state: {
    user,
    socket: {
      isConnected: false,
      message: '',
      reconnectError: false,
    },
  },
  mutations: {
    SOCKET_ONOPEN(state, event) {
      Vue.prototype.$socket = event.currentTarget;
      state.socket.isConnected = true;
    },
  },
});

Authentication

Session Vs Token

Authentication

Session - Django

radioify/views.py

class LoginView(APIView):
  
      permission_classes = (AllowAny, )
  
      def post(self, request, format=None):
          serializer = LoginSerializer(data=request.data)
          if serializer.is_valid():
              login(request, serializer.validated_data['user'])
              user_serializer = UserSerializer(serializer.validated_data['user'])
              return Response(user_serializer.data, status=200)
          return Response(None, status=401)
  
  
  class LogoutView(APIView):
  
      permission_classes = (IsAuthenticated, )
  
      def post(self, request, format=None):
        logout(request) 
          return Response(status=204)

Authentication

Session - Vue

front/src/store/index.js

const store = new Vuex.Store({
  mutations: {
    auth_success(state, data) {
      state.user = data;
      state.user.isAuthenticated = true;
    },
  },
  actions: {
    login({ commit }, data) {
      return new Promise((resolve, reject) => {
        Axios({
          url: '/auth/login/',
          data,
          method: 'POST',
        }).then((resp) => {
          commit('auth_success', resp.data);
          resolve(resp);
        }).catch((err) => {
          commit('auth_error');
          reject(err);
        });
      });
    },
    logout({ commit }) {
      return new Promise((resolve, reject) => {
        Axios({
          url: '/auth/logout/',
          data: {},
          method: 'POST',
        }).then(() => {
          commit('auth_loggedout');
        }).catch((err) => {
          commit('auth_error');
          reject(err);
        });
      });
    },
  },
});

Authentication

Session - Vue

front/src/router/index.js

const routes = [
  {
    path: '/podcasts/details/:id/',
    name: 'Episodes',
    component: Episodes,
    meta: {
      requiresAuth: true,
    },
  },
];

router.beforeEach((to, from, next) => {
  if (to.matched.some((record) => record.meta.requiresAuth) && !store.state.user.isAuthenticated) {
    next('/login/');
    return;
  }
  next();
});

Demo

Conclusions

  • Streaming is hard
  • Vue is quite similar to Django if you use VueX & VueXORM
  • Be mindful of local data vs API data
  • Mutations...

Questions

Creating a Spotify-like streaming service with Django and Vue

By Emma

Creating a Spotify-like streaming service with Django and Vue

DjangoCon EU 2020

  • 1,076