##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:
-
Once the user is logged in, they are redirected to the home page.
class HomeController < ApplicationController def index @suggestions = current_user.suggestions end end
-
'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
-
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
- These suggestions are returned to the controller, sent to the view, and iterated over as cards.
###Optimization:
- 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
-
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
-
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; } }