Skip to content

Instantly share code, notes, and snippets.

@Jlawlzz
Last active July 18, 2016 16:38
Show Gist options
  • Save Jlawlzz/d2b2331913872d1ec7dd5eb64f23005c to your computer and use it in GitHub Desktop.
Save Jlawlzz/d2b2331913872d1ec7dd5eb64f23005c to your computer and use it in GitHub Desktop.

##Models:

  • Album

  • Artist

  • Favorite

  • Follow

  • Genre

  • Like

  • PlaylistSongs

  • PlaylistTags

  • Playlist

  • Song

  • StaffPick

  • Tag

  • User

  • Suggestions[Folder]

    • Favorites
    • Follows
    • Likes
    • StaffPicks
  • Feed[Folder]

    • Favorite
    • Follow
    • Like
    • StaffPick
    • Advertisement

##Schema:

ActiveRecord::Schema.define(version: 20160717230059) do

  # These are extensions that must be enabled in order to support this database
  enable_extension "plpgsql"

  create_table "albums", force: :cascade do |t|
    t.string   "title"
    t.integer  "artist_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  add_index "albums", ["artist_id"], name: "index_albums_on_artist_id", using: :btree

  create_table "artists", force: :cascade do |t|
    t.string   "name"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.integer  "genre_id"
  end

  add_index "artists", ["genre_id"], name: "index_artists_on_genre_id", using: :btree

  create_table "genres", force: :cascade do |t|
    t.string   "type"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  create_table "playlist_songs", force: :cascade do |t|
    t.integer  "playlist_id"
    t.integer  "song_id"
    t.datetime "created_at",  null: false
    t.datetime "updated_at",  null: false
  end

  add_index "playlist_songs", ["playlist_id"], name: "index_playlist_songs_on_playlist_id", using: :btree
  add_index "playlist_songs", ["song_id"], name: "index_playlist_songs_on_song_id", using: :btree

  create_table "playlist_tags", force: :cascade do |t|
    t.integer  "playlist_id"
    t.integer  "tag_id"
    t.datetime "created_at",  null: false
    t.datetime "updated_at",  null: false
  end

  add_index "playlist_tags", ["playlist_id"], name: "index_playlist_tags_on_playlist_id", using: :btree
  add_index "playlist_tags", ["tag_id"], name: "index_playlist_tags_on_tag_id", using: :btree

  create_table "playlists", force: :cascade do |t|
    t.string   "name"
    t.integer  "user_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.boolean  "staff_pick"
  end

  add_index "playlists", ["user_id"], name: "index_playlists_on_user_id", using: :btree

  create_table "songs", force: :cascade do |t|
    t.string   "title"
    t.integer  "artist_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.integer  "album_id"
  end

  add_index "songs", ["album_id"], name: "index_songs_on_album_id", using: :btree
  add_index "songs", ["artist_id"], name: "index_songs_on_artist_id", using: :btree

  create_table "tags", force: :cascade do |t|
    t.string   "name"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  create_table "users", force: :cascade do |t|
    t.string   "name"
    t.string   "email"
    t.string   "password_digest"
    t.datetime "created_at",      null: false
    t.datetime "updated_at",      null: false
  end

  add_foreign_key "albums", "artists"
  add_foreign_key "artists", "genres"
  add_foreign_key "playlist_songs", "playlists"
  add_foreign_key "playlist_songs", "songs"
  add_foreign_key "playlist_tags", "playlists"
  add_foreign_key "playlist_tags", "tags"
  add_foreign_key "playlists", "users"
  add_foreign_key "songs", "albums"
  add_foreign_key "songs", "artists"
end

###How it works:

  1. Once the user is logged in, they are redirected to the home page.

     class HomeController < ApplicationController
       
       def index
         @suggestions = current_user.suggestions
       end
    
     end
    
  2. 'current_user.suggestions' first tries to pull the users suggestions from memory. If they cannot be, 'fetch_suggestions' is called and saved to the store with an expiration of 3 hours.

     class User < ActiveRecord::Base
       has_many :likes
       has_many :favorites
       has_many :followers
       
       has_secure_password
       
       def suggestions
         suggestions = $redis.get('suggestions')
           if suggestions.nil?
             suggestions = Suggestions.fetch(self).to_json
             $redis.set('suggestions', suggestions)
             $redis.expire('suggestions', 3.hour.to_i)
           end
           JSON.load suggestions
       end
       
       def fetch_suggestions
         suggestions = []
         (suggestions << Suggestions::Like.get_suggestions(self.likes.last(5)) if !self.likes.empty?
         (suggestions << Suggestions::Favorite.get_suggestions(self.favorites.last(5)) if !self.favorites.empty?
         (suggestions << Suggestions::Follow.get_suggestions(self.favorites.last(5)) if !self.follows.empty?
         (suggestions << Suggestions::StaffPicks.get_picks(50 - suggestions.count))
         suggestions.flatten!
       end
       
     end
    
  3. When 'fetch_suggestions' is hit, the last 5 (or less) records for each suggestion type are sent through a specialized class under a Suggestions namespace. The goal is to end with 10 suggestions for each type, and a total of 40 suggestions. "Suggestions::StaffPicks" fills in whatever slack is left in order to make the grand total of suggestions 50.

  • Example 1: Likes

      class Suggestions::Likes
    
        def self.get_suggestions(likes)
          amount = 10 / likes.count
          suggestions = likes.map do |like|
            tags = like.playlist.tags
            SuggestionService.recommend(tags, amount).map { |suggestion| Feed::Like.new(like, suggestion) }
          end
          suggestions.flatten!
        end
        
      end
      
      class Feed::Like
    
        attr_accessor :reason, :content, :type
        
        def initialize(like, suggested_playlist)
          @reason = "Because you liked #{like.playlist}"
          @type = 'like'
          @content = suggested_playlist
        end
        
      end
    
      class Like < ActiveRecord::Base
          belongs_to :user
          belongs_to :playlist
      end
    
  • Example 2: Favorites

      class Suggestions::Favorites
    
        def self.get_suggestions(favorites)
          amount = 10 / favorites.count
          suggestions = favorites.map do |favorite|
            tags = [favorite.song.artist.name] + [favorite.song.genres]
            SuggestionService.recommend(tags, amount).map { |suggestion| Feed::Favorite.new(favorite, suggestion) }
          end
          suggestions.flatten!
        end
        
      end
    
      class Feed::Favorite
    
        attr_accessor :reason, :content, :type
        
        def initialize(favorite, suggested_playlist)
          @reason = "Beacuse you favorited #{favorite.song.title}, by #{favorite.artist.name}"
          @type = 'favorite'
          @content = suggested_playlist
        end
        
      end
      
      class Favorite < ActiveRecord::Base
        belongs_to :user
        belongs_to :song
      end
    
  1. These suggestions are returned to the controller, sent to the view, and iterated over as cards.

###Optimization:

  1. Dynamic Pagination: Instead of loading all 50 suggestions to the homepage at once, implementing a dynamic scroll based pagination could speed things up considerably. An AJAX call would load the first 10 suggested playlists (page 1) onto the view. Once the user scrolls to the bottom of the page, an AJAX call would be made for page 2 of results and so on.
- AJAX call:

        $.ajax({
          url: "/api/v1/home/" + pageNumber,
          type: "GET",
          success: function(response){
            console.log("Success!", response.suggestions)
            appendSuggestions(response)
          }
        });

- API Controller:
  
        class Api::V1::HomeController < Api::ApiController
  
          respond_to :json
        
          def index
            respond_with current_user.get_suggestions(params[:page])
          end

        end

- Routes:
        
        namespace :api do

          namespace :v1, defaults: {format: :json} do
            get '/home/:page', to: 'home#index'
          end
        
        end
  1. Bypassing the rails app utilizing 'Metal' routed through Sinatra for the pagination call above, as well as other api calls would make things quite a bit faster:

         require 'sinatra'
         Sinatra::Application.default_options.merge!(:run => false, :env => :production)
         Api = Sinatra.application unless defined? Api
         
         get '/api/v1/home/:page' do
           #Gather paginated page
         end
    
  2. Ngyinx can be used to serve static assets like images or audio before they hit the app. While rails does have 'ActionDispatch::Static' it is rarely used due to its inefficiency. (new to ngyinx so example is rough and simplified)

         server {
           listen        8080;
           server_name   www.exampleurl.com exampleurl.com;
           error_page    404    /404.html;
         
           location /public/images {
             root  /home/www-data/example_url;
           } 
           
           location /public/audio {
             root /home/www-data/example_url;
           }
         
           location = /404.html {
             root /home/www-data/exampleurl/static/html;
           }       
         }
    
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment