Artist import, page
This commit is contained in:
parent
235c0b809e
commit
b5f616a9d9
|
@ -1,3 +1,4 @@
|
||||||
/config/database.yml
|
/config/database.yml
|
||||||
|
/config/api_keys.yml
|
||||||
/log/*.log
|
/log/*.log
|
||||||
/tmp
|
/tmp
|
||||||
|
|
7
Gemfile
7
Gemfile
|
@ -8,6 +8,7 @@ group :assets do
|
||||||
gem "coffee-rails", "~> 3.2.1"
|
gem "coffee-rails", "~> 3.2.1"
|
||||||
|
|
||||||
gem "therubyracer", platforms: :ruby
|
gem "therubyracer", platforms: :ruby
|
||||||
|
gem "hogan_assets"
|
||||||
|
|
||||||
gem "uglifier", ">= 1.0.3"
|
gem "uglifier", ">= 1.0.3"
|
||||||
end
|
end
|
||||||
|
@ -21,3 +22,9 @@ gem "thin"
|
||||||
|
|
||||||
# Deploy with Capistrano
|
# Deploy with Capistrano
|
||||||
gem "capistrano"
|
gem "capistrano"
|
||||||
|
|
||||||
|
gem "robbie", path: "../robbie"
|
||||||
|
gem "beatparser", path: "../beatparser"
|
||||||
|
gem "rails-backbone"
|
||||||
|
gem "eco"
|
||||||
|
gem 'bootstrap-sass', '~> 2.0.4.0'
|
||||||
|
|
74
Gemfile.lock
74
Gemfile.lock
|
@ -1,5 +1,20 @@
|
||||||
|
PATH
|
||||||
|
remote: ../beatparser
|
||||||
|
specs:
|
||||||
|
beatparser (0.0.1)
|
||||||
|
lastfm-client
|
||||||
|
musicbrainz
|
||||||
|
robbie
|
||||||
|
|
||||||
|
PATH
|
||||||
|
remote: ../robbie
|
||||||
|
specs:
|
||||||
|
robbie (0.0.1)
|
||||||
|
httparty
|
||||||
|
oj
|
||||||
|
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: http://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actionmailer (3.2.8)
|
actionmailer (3.2.8)
|
||||||
actionpack (= 3.2.8)
|
actionpack (= 3.2.8)
|
||||||
|
@ -29,7 +44,14 @@ GEM
|
||||||
i18n (~> 0.6)
|
i18n (~> 0.6)
|
||||||
multi_json (~> 1.0)
|
multi_json (~> 1.0)
|
||||||
arel (3.0.2)
|
arel (3.0.2)
|
||||||
|
bootstrap-sass (2.0.4.0)
|
||||||
builder (3.0.0)
|
builder (3.0.0)
|
||||||
|
capistrano (2.12.0)
|
||||||
|
highline
|
||||||
|
net-scp (>= 1.0.0)
|
||||||
|
net-sftp (>= 2.0.0)
|
||||||
|
net-ssh (>= 2.0.14)
|
||||||
|
net-ssh-gateway (>= 1.1.0)
|
||||||
coffee-rails (3.2.2)
|
coffee-rails (3.2.2)
|
||||||
coffee-script (>= 2.2.0)
|
coffee-script (>= 2.2.0)
|
||||||
railties (~> 3.2.0)
|
railties (~> 3.2.0)
|
||||||
|
@ -37,22 +59,53 @@ GEM
|
||||||
coffee-script-source
|
coffee-script-source
|
||||||
execjs
|
execjs
|
||||||
coffee-script-source (1.3.3)
|
coffee-script-source (1.3.3)
|
||||||
|
daemons (1.1.9)
|
||||||
|
eco (1.0.0)
|
||||||
|
coffee-script
|
||||||
|
eco-source
|
||||||
|
execjs
|
||||||
|
eco-source (1.1.0.rc.1)
|
||||||
|
ejs (1.0.0)
|
||||||
erubis (2.7.0)
|
erubis (2.7.0)
|
||||||
|
eventmachine (0.12.10)
|
||||||
execjs (1.4.0)
|
execjs (1.4.0)
|
||||||
multi_json (~> 1.0)
|
multi_json (~> 1.0)
|
||||||
|
highline (1.6.13)
|
||||||
hike (1.2.1)
|
hike (1.2.1)
|
||||||
|
hogan_assets (1.3.2)
|
||||||
|
execjs (>= 1.2.9)
|
||||||
|
sprockets (>= 2.0.3)
|
||||||
|
tilt (>= 1.3.3)
|
||||||
|
httparty (0.8.3)
|
||||||
|
multi_json (~> 1.0)
|
||||||
|
multi_xml
|
||||||
i18n (0.6.0)
|
i18n (0.6.0)
|
||||||
journey (1.0.4)
|
journey (1.0.4)
|
||||||
jquery-rails (2.1.1)
|
jquery-rails (2.1.1)
|
||||||
railties (>= 3.1.0, < 5.0)
|
railties (>= 3.1.0, < 5.0)
|
||||||
thor (~> 0.14)
|
thor (~> 0.14)
|
||||||
json (1.7.5)
|
json (1.7.5)
|
||||||
|
lastfm-client (0.0.3)
|
||||||
|
json (>= 1.4.6)
|
||||||
|
libv8 (3.3.10.4)
|
||||||
mail (2.4.4)
|
mail (2.4.4)
|
||||||
i18n (>= 0.4.0)
|
i18n (>= 0.4.0)
|
||||||
mime-types (~> 1.16)
|
mime-types (~> 1.16)
|
||||||
treetop (~> 1.4.8)
|
treetop (~> 1.4.8)
|
||||||
mime-types (1.19)
|
mime-types (1.19)
|
||||||
multi_json (1.3.6)
|
multi_json (1.3.6)
|
||||||
|
multi_xml (0.5.1)
|
||||||
|
musicbrainz (0.7.0)
|
||||||
|
nokogiri
|
||||||
|
net-scp (1.0.4)
|
||||||
|
net-ssh (>= 1.99.1)
|
||||||
|
net-sftp (2.0.5)
|
||||||
|
net-ssh (>= 2.0.9)
|
||||||
|
net-ssh (2.5.2)
|
||||||
|
net-ssh-gateway (1.1.0)
|
||||||
|
net-ssh (>= 1.99.1)
|
||||||
|
nokogiri (1.5.5)
|
||||||
|
oj (1.3.4)
|
||||||
pg (0.14.0)
|
pg (0.14.0)
|
||||||
polyglot (0.3.3)
|
polyglot (0.3.3)
|
||||||
rack (1.4.1)
|
rack (1.4.1)
|
||||||
|
@ -70,6 +123,10 @@ GEM
|
||||||
activesupport (= 3.2.8)
|
activesupport (= 3.2.8)
|
||||||
bundler (~> 1.0)
|
bundler (~> 1.0)
|
||||||
railties (= 3.2.8)
|
railties (= 3.2.8)
|
||||||
|
rails-backbone (0.7.2)
|
||||||
|
coffee-script (~> 2.2.0)
|
||||||
|
ejs (~> 1.0.0)
|
||||||
|
railties (>= 3.1.0)
|
||||||
railties (3.2.8)
|
railties (3.2.8)
|
||||||
actionpack (= 3.2.8)
|
actionpack (= 3.2.8)
|
||||||
activesupport (= 3.2.8)
|
activesupport (= 3.2.8)
|
||||||
|
@ -89,6 +146,12 @@ GEM
|
||||||
hike (~> 1.2)
|
hike (~> 1.2)
|
||||||
rack (~> 1.0)
|
rack (~> 1.0)
|
||||||
tilt (~> 1.1, != 1.3.0)
|
tilt (~> 1.1, != 1.3.0)
|
||||||
|
therubyracer (0.10.2)
|
||||||
|
libv8 (~> 3.3.10)
|
||||||
|
thin (1.4.1)
|
||||||
|
daemons (>= 1.0.9)
|
||||||
|
eventmachine (>= 0.12.6)
|
||||||
|
rack (>= 1.0.0)
|
||||||
thor (0.16.0)
|
thor (0.16.0)
|
||||||
tilt (1.3.3)
|
tilt (1.3.3)
|
||||||
treetop (1.4.10)
|
treetop (1.4.10)
|
||||||
|
@ -103,9 +166,18 @@ PLATFORMS
|
||||||
ruby
|
ruby
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
|
beatparser!
|
||||||
|
bootstrap-sass (~> 2.0.4.0)
|
||||||
|
capistrano
|
||||||
coffee-rails (~> 3.2.1)
|
coffee-rails (~> 3.2.1)
|
||||||
|
eco
|
||||||
|
hogan_assets
|
||||||
jquery-rails
|
jquery-rails
|
||||||
pg
|
pg
|
||||||
rails (= 3.2.8)
|
rails (= 3.2.8)
|
||||||
|
rails-backbone
|
||||||
|
robbie!
|
||||||
sass-rails (~> 3.2.3)
|
sass-rails (~> 3.2.3)
|
||||||
|
therubyracer
|
||||||
|
thin
|
||||||
uglifier (>= 1.0.3)
|
uglifier (>= 1.0.3)
|
||||||
|
|
|
@ -12,4 +12,11 @@
|
||||||
//
|
//
|
||||||
//= require jquery
|
//= require jquery
|
||||||
//= require jquery_ujs
|
//= require jquery_ujs
|
||||||
|
//= require mustache
|
||||||
|
//= require hogan
|
||||||
|
//= require underscore
|
||||||
|
//= require backbone
|
||||||
|
//= require backbone_rails_sync
|
||||||
|
//= require backbone_datalink
|
||||||
|
//= require backbone/beat_haven
|
||||||
//= require_tree .
|
//= require_tree .
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
#= require_self
|
||||||
|
#= require_tree ./templates
|
||||||
|
#= require_tree ./models
|
||||||
|
#= require_tree ./views
|
||||||
|
#= require_tree ./routers
|
||||||
|
|
||||||
|
window.BeatHaven =
|
||||||
|
Models: {}
|
||||||
|
Collections: {}
|
||||||
|
Routers: {}
|
||||||
|
Views: {}
|
||||||
|
|
||||||
|
init: ->
|
||||||
|
new BeatHaven.Routers.Artist()
|
||||||
|
Backbone.history.start();
|
||||||
|
|
||||||
|
$ ->
|
||||||
|
BeatHaven.init()
|
|
@ -0,0 +1,2 @@
|
||||||
|
class BeatHaven.Models.Artist extends Backbone.Model
|
||||||
|
urlRoot: "/api/artists"
|
|
@ -0,0 +1,9 @@
|
||||||
|
class BeatHaven.Routers.Artist extends Backbone.Router
|
||||||
|
routes:
|
||||||
|
"artist/:name": "show"
|
||||||
|
|
||||||
|
show: (name) ->
|
||||||
|
artist = new BeatHaven.Models.Artist(id: name)
|
||||||
|
artist.fetch()
|
||||||
|
view = new BeatHaven.Views.ArtistShow(model: artist)
|
||||||
|
$("#main").html(view.render().el)
|
|
@ -0,0 +1,39 @@
|
||||||
|
<div class="artist-page">
|
||||||
|
<h1>{{name}}</h1>
|
||||||
|
<div class="artist-info">
|
||||||
|
<div class="pic">
|
||||||
|
<img src="{{pic}}" alt="{{name}}">
|
||||||
|
</div>
|
||||||
|
<div class="bio">{{bio}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="albums">
|
||||||
|
{{#albums}}
|
||||||
|
<div class="album">
|
||||||
|
<div class="pic">
|
||||||
|
<img src="{{pic_safe}}" alt="{{title}}"><br>
|
||||||
|
<a href="" class="btn btn-success play-all"><i class="icon-plus icon-white"></i> Add to playlist</a>
|
||||||
|
<a href="" class="btn btn-info play-all"><i class="icon-play icon-white"></i> Play</a>
|
||||||
|
</div>
|
||||||
|
<h2 class="title">{{title}} ({{year}})</h2>
|
||||||
|
<div class="tracks">
|
||||||
|
<table class="table">
|
||||||
|
<tbody>
|
||||||
|
{{#tracks}}
|
||||||
|
<tr>
|
||||||
|
<td class="title">
|
||||||
|
<a href="" class="btn btn-round track-play"><i class="icon-play"></i></a>
|
||||||
|
<a href="" class="track-link">{{title}}</a>
|
||||||
|
</td>
|
||||||
|
<td class="length">
|
||||||
|
<span class="length">{{length}}</span>
|
||||||
|
<a href="" class="btn btn-round track-add"><i class="icon-plus"></i></a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{/tracks}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/albums}}
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,10 @@
|
||||||
|
class BeatHaven.Views.ArtistShow extends Backbone.View
|
||||||
|
template: HoganTemplates["backbone/templates/artists/show"]
|
||||||
|
|
||||||
|
initialize: ->
|
||||||
|
@model.on("change", @render, this)
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
return this if typeof @model.attributes.id is "string"
|
||||||
|
$(@el).html(@template.render(@model.toJSON()))
|
||||||
|
this
|
|
@ -1,13 +0,0 @@
|
||||||
/*
|
|
||||||
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
|
||||||
* listed below.
|
|
||||||
*
|
|
||||||
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
|
||||||
* or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
|
|
||||||
*
|
|
||||||
* You're free to add application-wide styles to this file and they'll appear at the top of the
|
|
||||||
* compiled file, but it's generally better to create a new file per style scope.
|
|
||||||
*
|
|
||||||
*= require_self
|
|
||||||
*= require_tree .
|
|
||||||
*/
|
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,132 @@
|
||||||
|
.artist-page {
|
||||||
|
h1 {
|
||||||
|
font-family: Lobster, Georgia, serif;
|
||||||
|
font-size: 42px;
|
||||||
|
line-height: 64px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
text-shadow: 1px 1px 0 rgba(0, 0, 0, .2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.artist-info {
|
||||||
|
min-height: 250px;
|
||||||
|
margin-bottom: 50px;
|
||||||
|
|
||||||
|
.pic {
|
||||||
|
float: left;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 250px;
|
||||||
|
height: 250px;
|
||||||
|
border-size: 1px;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: rgba(255, 255, 255, .2);
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 1px 1px 5px rgba(0, 0, 0, .2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.bio {
|
||||||
|
margin-left: 270px;
|
||||||
|
font-family: "Source Sans Pro", Helvetica, sans-serif;
|
||||||
|
font-size: 22px;
|
||||||
|
line-height: 26px;
|
||||||
|
text-shadow: 1px 1px 0 rgba(0, 0, 0, .1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.album {
|
||||||
|
min-height: 250px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-family: Lobster, Georgia, Serif;
|
||||||
|
font-size: 26px;
|
||||||
|
text-shadow: 1px 1px 0 rgba(0, 0, 0, .2);
|
||||||
|
}
|
||||||
|
.pic {
|
||||||
|
float: left;
|
||||||
|
height: 300px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
img {
|
||||||
|
background-color: #a0a0a0;
|
||||||
|
width: 250px;
|
||||||
|
height: 250px;
|
||||||
|
border-size: 1px;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: rgba(255, 255, 255, .2);
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 1px 1px 5px rgba(0, 0, 0, .2);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
margin-left: 270px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.tracks {
|
||||||
|
margin-left: 270px;
|
||||||
|
|
||||||
|
tr:hover {
|
||||||
|
td {
|
||||||
|
background-color: rgba(200, 200, 200, .3);
|
||||||
|
|
||||||
|
.track-play i {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
&.length {
|
||||||
|
.track-add {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.length {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
position: relative;
|
||||||
|
font-family: "Source Sans Pro", Helvetica, sans-serif;
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 28px;
|
||||||
|
text-shadow: 1px 1px 0 rgba(0, 0, 0, .1);
|
||||||
|
|
||||||
|
.btn-round {
|
||||||
|
width: 10px;
|
||||||
|
height: 22px;
|
||||||
|
border-radius: 50px;
|
||||||
|
|
||||||
|
i {
|
||||||
|
margin: 3px 0 0 -1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
td.title {
|
||||||
|
padding-left: 40px;
|
||||||
|
.track-play {
|
||||||
|
position: absolute;
|
||||||
|
margin: -2px 0 0 -40px;
|
||||||
|
|
||||||
|
i {
|
||||||
|
opacity: .3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.track-link {
|
||||||
|
color: #303030;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
td.length {
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
.track-add {
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
margin: -2px 0 0 0;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
module Api
|
||||||
|
class AlbumsController < ::ApplicationController
|
||||||
|
def picture
|
||||||
|
album = Album.find(params[:id])
|
||||||
|
redirect_to album.load_pic
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,10 @@
|
||||||
|
module Api
|
||||||
|
class ArtistsController < ::ApplicationController
|
||||||
|
def show
|
||||||
|
artist = Artist.with_name(params[:id].gsub("+", " "))
|
||||||
|
return render json: { fail: true } if artist.nil?
|
||||||
|
|
||||||
|
render json: artist.dump_json
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,3 +1,7 @@
|
||||||
class ApplicationController < ActionController::Base
|
class ApplicationController < ActionController::Base
|
||||||
protect_from_forgery
|
protect_from_forgery
|
||||||
|
|
||||||
|
def main
|
||||||
|
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
class Album < ActiveRecord::Base
|
||||||
|
belongs_to :artist
|
||||||
|
has_many :tracks
|
||||||
|
|
||||||
|
attr_accessible :artist_id, :pic, :rovi_id, :title, :year
|
||||||
|
|
||||||
|
def pic_safe
|
||||||
|
unless pic.nil?
|
||||||
|
pic
|
||||||
|
else
|
||||||
|
"/api/albums/#{id}/picture"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_pic
|
||||||
|
info = BeatParser::Sources::Lastfm.album_info(artist.name, title)
|
||||||
|
unless info[:pic].nil?
|
||||||
|
update_attributes(pic: info[:pic])
|
||||||
|
info[:pic]
|
||||||
|
else
|
||||||
|
"/assets/images/album-dummy.png"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,88 @@
|
||||||
|
class Artist < ActiveRecord::Base
|
||||||
|
has_many :albums
|
||||||
|
has_many :performers
|
||||||
|
has_many :tracks, through: :performers
|
||||||
|
has_many :genres, through: :artist_genres
|
||||||
|
|
||||||
|
attr_accessible :bio, :is_group, :name, :pic, :rovi_id
|
||||||
|
|
||||||
|
def loaded?
|
||||||
|
pic? && bio?
|
||||||
|
end
|
||||||
|
|
||||||
|
def dump_json
|
||||||
|
serialized = to_json(
|
||||||
|
include: {
|
||||||
|
albums: {
|
||||||
|
include: {
|
||||||
|
tracks: {
|
||||||
|
methods: [:length],
|
||||||
|
except: [:created_at, :updated_at, :rovi_id, :album_id]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
except: [:created_at, :updated_at, :rovi_id, :pic],
|
||||||
|
methods: [:pic_safe]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
except: [:created_at, :updated_at, :rovi_id],
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
class << self
|
||||||
|
def with_name(name)
|
||||||
|
# DB lookup
|
||||||
|
artist = find_by_name(name)
|
||||||
|
return artist unless artist.nil?
|
||||||
|
|
||||||
|
# Rovi correction
|
||||||
|
rovi_artist = Robbie::Artist.find_by_name(name)
|
||||||
|
return artist if rovi_artist && artist = find_by_rovi_id(rovi_artist.id)
|
||||||
|
|
||||||
|
# Parsing artist if ok
|
||||||
|
import(rovi_artist) if rovi_artist
|
||||||
|
end
|
||||||
|
|
||||||
|
def import(rovi_artist)
|
||||||
|
data = BeatParser::Aggregator.new.combine(rovi_artist.id)
|
||||||
|
artist = Artist.find_or_create_by_rovi_id(data[:id])
|
||||||
|
artist.update_attributes(
|
||||||
|
name: data[:name],
|
||||||
|
is_group: data[:is_group],
|
||||||
|
pic: data[:pic],
|
||||||
|
bio: data[:bio]
|
||||||
|
)
|
||||||
|
data[:albums].each do |album_meta|
|
||||||
|
album = Album.find_or_create_by_rovi_id(album_meta[:id])
|
||||||
|
album.update_attributes(
|
||||||
|
artist_id: artist.id,
|
||||||
|
title: album_meta[:title],
|
||||||
|
year: album_meta[:year].to_i
|
||||||
|
)
|
||||||
|
album_meta[:tracks].each do |track_meta|
|
||||||
|
track = Track.find_or_create_by_rovi_id(track_meta[:id])
|
||||||
|
track.update_attributes(
|
||||||
|
album_id: album.id,
|
||||||
|
disc_id: track_meta[:disc_id],
|
||||||
|
position: track_meta[:position],
|
||||||
|
title: track_meta[:title],
|
||||||
|
duration: track_meta[:duration]
|
||||||
|
)
|
||||||
|
track_meta[:artists].each do |performer|
|
||||||
|
performer_artist = Artist.find_or_create_by_rovi_id(performer[:id])
|
||||||
|
performer_artist.update_attributes(
|
||||||
|
name: performer[:name]
|
||||||
|
)
|
||||||
|
Performer.find_or_create_by_artist_id_and_track_id(performer_artist.id, track.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
data[:genres].each do |genre_meta|
|
||||||
|
genre = Genre.find_or_create_by_rovi_id(genre_meta[:id])
|
||||||
|
genre.update_attributes(
|
||||||
|
name: genre_meta[:name]
|
||||||
|
)
|
||||||
|
ArtistGenre.find_or_create_by_artist_id_and_genre_id(artist.id, genre.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,3 @@
|
||||||
|
class ArtistGenre < ActiveRecord::Base
|
||||||
|
attr_accessible :artist_id, :genre_id
|
||||||
|
end
|
|
@ -0,0 +1,3 @@
|
||||||
|
class Genre < ActiveRecord::Base
|
||||||
|
attr_accessible :name, :rovi_id
|
||||||
|
end
|
|
@ -0,0 +1,6 @@
|
||||||
|
class Performer < ActiveRecord::Base
|
||||||
|
belongs_to :artist
|
||||||
|
belongs_to :track
|
||||||
|
|
||||||
|
attr_accessible :artist_id, :track_id
|
||||||
|
end
|
|
@ -0,0 +1,14 @@
|
||||||
|
class Track < ActiveRecord::Base
|
||||||
|
belongs_to :album
|
||||||
|
has_many :performers
|
||||||
|
has_many :artists, through: :performers
|
||||||
|
|
||||||
|
attr_accessible :album_id, :disc_id, :duration, :position, :rovi_id, :title
|
||||||
|
|
||||||
|
def length
|
||||||
|
return if duration.nil?
|
||||||
|
length = duration.divmod(60).map(&:to_s)
|
||||||
|
length[1] = "0" << length[1] if length[1].length == 1
|
||||||
|
length.join(":")
|
||||||
|
end
|
||||||
|
end
|
|
@ -2,13 +2,20 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>BeatHaven</title>
|
<title>BeatHaven</title>
|
||||||
<%= stylesheet_link_tag "application", :media => "all" %>
|
<%= stylesheet_link_tag "application", media: "all" %>
|
||||||
<%= javascript_include_tag "application" %>
|
<%= javascript_include_tag "application" %>
|
||||||
<%= csrf_meta_tags %>
|
<%= csrf_meta_tags %>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<%= yield %>
|
<div class="navbar navbar-fixed-top">
|
||||||
|
<div class="navbar-inner">
|
||||||
|
<div class="container">
|
||||||
|
<a class="brand" href="">BeatHaven</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="container" id="main">Loading...</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
lastfm:
|
||||||
|
api_key: secret
|
||||||
|
api_secret: secret
|
||||||
|
client_name: BeatHaven
|
||||||
|
rovi:
|
||||||
|
api_key: secret
|
||||||
|
api_secret: secret
|
|
@ -0,0 +1,13 @@
|
||||||
|
File.open("#{Rails.root}/config/api_keys.yml") do |file|
|
||||||
|
config = YAML.load(file.read)
|
||||||
|
|
||||||
|
LastFM.api_key = config["lastfm"]["api_key"]
|
||||||
|
LastFM.secret = config["lastfm"]["api_secret"]
|
||||||
|
LastFM.client_name = config["lastfm"]["client_name"]
|
||||||
|
|
||||||
|
Robbie.setup(
|
||||||
|
api_key: config["rovi"]["api_key"],
|
||||||
|
api_secret: config["rovi"]["api_secret"]
|
||||||
|
)
|
||||||
|
Robbie.enable_cache
|
||||||
|
end
|
|
@ -0,0 +1,3 @@
|
||||||
|
HoganAssets::Config.configure do |config|
|
||||||
|
config.path_prefix = "backbone/templates/"
|
||||||
|
end
|
|
@ -1,58 +1,10 @@
|
||||||
BeatHaven::Application.routes.draw do
|
BeatHaven::Application.routes.draw do
|
||||||
# The priority is based upon order of creation:
|
namespace :api do
|
||||||
# first created -> highest priority.
|
resources :artists, only: [:show], constraints: { id: /.+/ }
|
||||||
|
resources :albums, only: [:picture] do
|
||||||
|
member { get :picture }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Sample of regular route:
|
root to: "application#main"
|
||||||
# match 'products/:id' => 'catalog#view'
|
|
||||||
# Keep in mind you can assign values other than :controller and :action
|
|
||||||
|
|
||||||
# Sample of named route:
|
|
||||||
# match 'products/:id/purchase' => 'catalog#purchase', :as => :purchase
|
|
||||||
# This route can be invoked with purchase_url(:id => product.id)
|
|
||||||
|
|
||||||
# Sample resource route (maps HTTP verbs to controller actions automatically):
|
|
||||||
# resources :products
|
|
||||||
|
|
||||||
# Sample resource route with options:
|
|
||||||
# resources :products do
|
|
||||||
# member do
|
|
||||||
# get 'short'
|
|
||||||
# post 'toggle'
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# collection do
|
|
||||||
# get 'sold'
|
|
||||||
# end
|
|
||||||
# end
|
|
||||||
|
|
||||||
# Sample resource route with sub-resources:
|
|
||||||
# resources :products do
|
|
||||||
# resources :comments, :sales
|
|
||||||
# resource :seller
|
|
||||||
# end
|
|
||||||
|
|
||||||
# Sample resource route with more complex sub-resources
|
|
||||||
# resources :products do
|
|
||||||
# resources :comments
|
|
||||||
# resources :sales do
|
|
||||||
# get 'recent', :on => :collection
|
|
||||||
# end
|
|
||||||
# end
|
|
||||||
|
|
||||||
# Sample resource route within a namespace:
|
|
||||||
# namespace :admin do
|
|
||||||
# # Directs /admin/products/* to Admin::ProductsController
|
|
||||||
# # (app/controllers/admin/products_controller.rb)
|
|
||||||
# resources :products
|
|
||||||
# end
|
|
||||||
|
|
||||||
# You can have the root of your site routed with "root"
|
|
||||||
# just remember to delete public/index.html.
|
|
||||||
# root :to => 'welcome#index'
|
|
||||||
|
|
||||||
# See how all your routes lay out with "rake routes"
|
|
||||||
|
|
||||||
# This is a legacy wild controller route that's not recommended for RESTful applications.
|
|
||||||
# Note: This route will make all actions in every controller accessible via GET requests.
|
|
||||||
# match ':controller(/:action(/:id))(.:format)'
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
class CreateGenres < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
create_table :genres do |t|
|
||||||
|
t.string :rovi_id
|
||||||
|
t.string :name
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,10 @@
|
||||||
|
class CreateArtistGenres < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
create_table :artist_genres do |t|
|
||||||
|
t.integer :artist_id
|
||||||
|
t.integer :genre_id
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,13 @@
|
||||||
|
class CreateArtists < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
create_table :artists do |t|
|
||||||
|
t.string :rovi_id
|
||||||
|
t.string :name
|
||||||
|
t.boolean :is_group
|
||||||
|
t.text :bio
|
||||||
|
t.string :pic
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,13 @@
|
||||||
|
class CreateAlbums < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
create_table :albums do |t|
|
||||||
|
t.integer :artist_id
|
||||||
|
t.string :rovi_id
|
||||||
|
t.string :title
|
||||||
|
t.integer :year
|
||||||
|
t.string :pic
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,14 @@
|
||||||
|
class CreateTracks < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
create_table :tracks do |t|
|
||||||
|
t.integer :album_id
|
||||||
|
t.string :rovi_id
|
||||||
|
t.integer :disc_id
|
||||||
|
t.integer :position
|
||||||
|
t.string :title
|
||||||
|
t.integer :duration
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,10 @@
|
||||||
|
class CreatePerformers < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
create_table :performers do |t|
|
||||||
|
t.integer :track_id
|
||||||
|
t.integer :artist_id
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,68 @@
|
||||||
|
# encoding: UTF-8
|
||||||
|
# This file is auto-generated from the current state of the database. Instead
|
||||||
|
# of editing this file, please use the migrations feature of Active Record to
|
||||||
|
# incrementally modify your database, and then regenerate this schema definition.
|
||||||
|
#
|
||||||
|
# Note that this schema.rb definition is the authoritative source for your
|
||||||
|
# database schema. If you need to create the application database on another
|
||||||
|
# system, you should be using db:schema:load, not running all the migrations
|
||||||
|
# from scratch. The latter is a flawed and unsustainable approach (the more migrations
|
||||||
|
# you'll amass, the slower it'll run and the greater likelihood for issues).
|
||||||
|
#
|
||||||
|
# It's strongly recommended to check this file into your version control system.
|
||||||
|
|
||||||
|
ActiveRecord::Schema.define(:version => 20120826172120) do
|
||||||
|
|
||||||
|
create_table "albums", :force => true do |t|
|
||||||
|
t.integer "artist_id"
|
||||||
|
t.string "rovi_id"
|
||||||
|
t.string "title"
|
||||||
|
t.integer "year"
|
||||||
|
t.string "pic"
|
||||||
|
t.datetime "created_at", :null => false
|
||||||
|
t.datetime "updated_at", :null => false
|
||||||
|
end
|
||||||
|
|
||||||
|
create_table "artist_genres", :force => true do |t|
|
||||||
|
t.integer "artist_id"
|
||||||
|
t.integer "genre_id"
|
||||||
|
t.datetime "created_at", :null => false
|
||||||
|
t.datetime "updated_at", :null => false
|
||||||
|
end
|
||||||
|
|
||||||
|
create_table "artists", :force => true do |t|
|
||||||
|
t.string "rovi_id"
|
||||||
|
t.string "name"
|
||||||
|
t.boolean "is_group"
|
||||||
|
t.text "bio"
|
||||||
|
t.string "pic"
|
||||||
|
t.datetime "created_at", :null => false
|
||||||
|
t.datetime "updated_at", :null => false
|
||||||
|
end
|
||||||
|
|
||||||
|
create_table "genres", :force => true do |t|
|
||||||
|
t.string "rovi_id"
|
||||||
|
t.string "name"
|
||||||
|
t.datetime "created_at", :null => false
|
||||||
|
t.datetime "updated_at", :null => false
|
||||||
|
end
|
||||||
|
|
||||||
|
create_table "performers", :force => true do |t|
|
||||||
|
t.integer "track_id"
|
||||||
|
t.integer "artist_id"
|
||||||
|
t.datetime "created_at", :null => false
|
||||||
|
t.datetime "updated_at", :null => false
|
||||||
|
end
|
||||||
|
|
||||||
|
create_table "tracks", :force => true do |t|
|
||||||
|
t.integer "album_id"
|
||||||
|
t.string "rovi_id"
|
||||||
|
t.integer "disc_id"
|
||||||
|
t.integer "position"
|
||||||
|
t.string "title"
|
||||||
|
t.integer "duration"
|
||||||
|
t.datetime "created_at", :null => false
|
||||||
|
t.datetime "updated_at", :null => false
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
|
@ -1,241 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Ruby on Rails: Welcome aboard</title>
|
|
||||||
<style type="text/css" media="screen">
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
margin-bottom: 25px;
|
|
||||||
padding: 0;
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
font-family: "Lucida Grande", "Bitstream Vera Sans", "Verdana";
|
|
||||||
font-size: 13px;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 28px;
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {color: #03c}
|
|
||||||
a:hover {
|
|
||||||
background-color: #03c;
|
|
||||||
color: white;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#page {
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
width: 750px;
|
|
||||||
margin: 0;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#content {
|
|
||||||
float: left;
|
|
||||||
background-color: white;
|
|
||||||
border: 3px solid #aaa;
|
|
||||||
border-top: none;
|
|
||||||
padding: 25px;
|
|
||||||
width: 500px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#sidebar {
|
|
||||||
float: right;
|
|
||||||
width: 175px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#footer {
|
|
||||||
clear: both;
|
|
||||||
}
|
|
||||||
|
|
||||||
#header, #about, #getting-started {
|
|
||||||
padding-left: 75px;
|
|
||||||
padding-right: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#header {
|
|
||||||
background-image: url("assets/rails.png");
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: top left;
|
|
||||||
height: 64px;
|
|
||||||
}
|
|
||||||
#header h1, #header h2 {margin: 0}
|
|
||||||
#header h2 {
|
|
||||||
color: #888;
|
|
||||||
font-weight: normal;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#about h3 {
|
|
||||||
margin: 0;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#about-content {
|
|
||||||
background-color: #ffd;
|
|
||||||
border: 1px solid #fc0;
|
|
||||||
margin-left: -55px;
|
|
||||||
margin-right: -10px;
|
|
||||||
}
|
|
||||||
#about-content table {
|
|
||||||
margin-top: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
font-size: 11px;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
#about-content td {
|
|
||||||
padding: 10px;
|
|
||||||
padding-top: 3px;
|
|
||||||
padding-bottom: 3px;
|
|
||||||
}
|
|
||||||
#about-content td.name {color: #555}
|
|
||||||
#about-content td.value {color: #000}
|
|
||||||
|
|
||||||
#about-content ul {
|
|
||||||
padding: 0;
|
|
||||||
list-style-type: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#about-content.failure {
|
|
||||||
background-color: #fcc;
|
|
||||||
border: 1px solid #f00;
|
|
||||||
}
|
|
||||||
#about-content.failure p {
|
|
||||||
margin: 0;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#getting-started {
|
|
||||||
border-top: 1px solid #ccc;
|
|
||||||
margin-top: 25px;
|
|
||||||
padding-top: 15px;
|
|
||||||
}
|
|
||||||
#getting-started h1 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
#getting-started h2 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: normal;
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 25px;
|
|
||||||
}
|
|
||||||
#getting-started ol {
|
|
||||||
margin-left: 0;
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
#getting-started li {
|
|
||||||
font-size: 18px;
|
|
||||||
color: #888;
|
|
||||||
margin-bottom: 25px;
|
|
||||||
}
|
|
||||||
#getting-started li h2 {
|
|
||||||
margin: 0;
|
|
||||||
font-weight: normal;
|
|
||||||
font-size: 18px;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
#getting-started li p {
|
|
||||||
color: #555;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#sidebar ul {
|
|
||||||
margin-left: 0;
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
#sidebar ul h3 {
|
|
||||||
margin-top: 25px;
|
|
||||||
font-size: 16px;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
border-bottom: 1px solid #ccc;
|
|
||||||
}
|
|
||||||
#sidebar li {
|
|
||||||
list-style-type: none;
|
|
||||||
}
|
|
||||||
#sidebar ul.links li {
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filename {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<script type="text/javascript">
|
|
||||||
function about() {
|
|
||||||
info = document.getElementById('about-content');
|
|
||||||
if (window.XMLHttpRequest)
|
|
||||||
{ xhr = new XMLHttpRequest(); }
|
|
||||||
else
|
|
||||||
{ xhr = new ActiveXObject("Microsoft.XMLHTTP"); }
|
|
||||||
xhr.open("GET","rails/info/properties",false);
|
|
||||||
xhr.send("");
|
|
||||||
info.innerHTML = xhr.responseText;
|
|
||||||
info.style.display = 'block'
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="page">
|
|
||||||
<div id="sidebar">
|
|
||||||
<ul id="sidebar-items">
|
|
||||||
<li>
|
|
||||||
<h3>Browse the documentation</h3>
|
|
||||||
<ul class="links">
|
|
||||||
<li><a href="http://guides.rubyonrails.org/">Rails Guides</a></li>
|
|
||||||
<li><a href="http://api.rubyonrails.org/">Rails API</a></li>
|
|
||||||
<li><a href="http://www.ruby-doc.org/core/">Ruby core</a></li>
|
|
||||||
<li><a href="http://www.ruby-doc.org/stdlib/">Ruby standard library</a></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="content">
|
|
||||||
<div id="header">
|
|
||||||
<h1>Welcome aboard</h1>
|
|
||||||
<h2>You’re riding Ruby on Rails!</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="about">
|
|
||||||
<h3><a href="rails/info/properties" onclick="about(); return false">About your application’s environment</a></h3>
|
|
||||||
<div id="about-content" style="display: none"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="getting-started">
|
|
||||||
<h1>Getting started</h1>
|
|
||||||
<h2>Here’s how to get rolling:</h2>
|
|
||||||
|
|
||||||
<ol>
|
|
||||||
<li>
|
|
||||||
<h2>Use <code>rails generate</code> to create your models and controllers</h2>
|
|
||||||
<p>To see all available options, run it without parameters.</p>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<h2>Set up a default route and remove <span class="filename">public/index.html</span></h2>
|
|
||||||
<p>Routes are set up in <span class="filename">config/routes.rb</span>.</p>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<h2>Create your database</h2>
|
|
||||||
<p>Run <code>rake db:create</code> to create your database. If you're not using SQLite (the default), edit <span class="filename">config/database.yml</span> with your username and password.</p>
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="footer"> </div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -0,0 +1,613 @@
|
||||||
|
/*!
|
||||||
|
* mustache.js - Logic-less {{mustache}} templates with JavaScript
|
||||||
|
* http://github.com/janl/mustache.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*global define: false*/
|
||||||
|
|
||||||
|
var Mustache;
|
||||||
|
|
||||||
|
(function (exports) {
|
||||||
|
if (typeof module !== "undefined" && typeof module.exports !== "undefined") {
|
||||||
|
module.exports = exports; // CommonJS
|
||||||
|
} else if (typeof define === "function") {
|
||||||
|
define(exports); // AMD
|
||||||
|
} else {
|
||||||
|
Mustache = exports; // <script>
|
||||||
|
}
|
||||||
|
}((function () {
|
||||||
|
var exports = {};
|
||||||
|
|
||||||
|
exports.name = "mustache.js";
|
||||||
|
exports.version = "0.5.2";
|
||||||
|
exports.tags = ["{{", "}}"];
|
||||||
|
|
||||||
|
exports.parse = parse;
|
||||||
|
exports.clearCache = clearCache;
|
||||||
|
exports.compile = compile;
|
||||||
|
exports.compilePartial = compilePartial;
|
||||||
|
exports.render = render;
|
||||||
|
|
||||||
|
exports.Scanner = Scanner;
|
||||||
|
exports.Context = Context;
|
||||||
|
exports.Renderer = Renderer;
|
||||||
|
|
||||||
|
// This is here for backwards compatibility with 0.4.x.
|
||||||
|
exports.to_html = function (template, view, partials, send) {
|
||||||
|
var result = render(template, view, partials);
|
||||||
|
|
||||||
|
if (typeof send === "function") {
|
||||||
|
send(result);
|
||||||
|
} else {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var whiteRe = /\s*/;
|
||||||
|
var spaceRe = /\s+/;
|
||||||
|
var nonSpaceRe = /\S/;
|
||||||
|
var eqRe = /\s*=/;
|
||||||
|
var curlyRe = /\s*\}/;
|
||||||
|
var tagRe = /#|\^|\/|>|\{|&|=|!/;
|
||||||
|
|
||||||
|
// Workaround for https://issues.apache.org/jira/browse/COUCHDB-577
|
||||||
|
// See https://github.com/janl/mustache.js/issues/189
|
||||||
|
function testRe(re, string) {
|
||||||
|
return RegExp.prototype.test.call(re, string);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isWhitespace(string) {
|
||||||
|
return !testRe(nonSpaceRe, string);
|
||||||
|
}
|
||||||
|
|
||||||
|
var isArray = Array.isArray || function (obj) {
|
||||||
|
return Object.prototype.toString.call(obj) === "[object Array]";
|
||||||
|
};
|
||||||
|
|
||||||
|
// OSWASP Guidelines: escape all non alphanumeric characters in ASCII space.
|
||||||
|
var jsCharsRe = /[\x00-\x2F\x3A-\x40\x5B-\x60\x7B-\xFF\u2028\u2029]/gm;
|
||||||
|
|
||||||
|
function quote(text) {
|
||||||
|
var escaped = text.replace(jsCharsRe, function (c) {
|
||||||
|
return "\\u" + ('0000' + c.charCodeAt(0).toString(16)).slice(-4);
|
||||||
|
});
|
||||||
|
|
||||||
|
return '"' + escaped + '"';
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeRe(string) {
|
||||||
|
return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
|
||||||
|
}
|
||||||
|
|
||||||
|
var entityMap = {
|
||||||
|
"&": "&",
|
||||||
|
"<": "<",
|
||||||
|
">": ">",
|
||||||
|
'"': '"',
|
||||||
|
"'": ''',
|
||||||
|
"/": '/'
|
||||||
|
};
|
||||||
|
|
||||||
|
function escapeHtml(string) {
|
||||||
|
return String(string).replace(/[&<>"'\/]/g, function (s) {
|
||||||
|
return entityMap[s];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export these utility functions.
|
||||||
|
exports.isWhitespace = isWhitespace;
|
||||||
|
exports.isArray = isArray;
|
||||||
|
exports.quote = quote;
|
||||||
|
exports.escapeRe = escapeRe;
|
||||||
|
exports.escapeHtml = escapeHtml;
|
||||||
|
|
||||||
|
function Scanner(string) {
|
||||||
|
this.string = string;
|
||||||
|
this.tail = string;
|
||||||
|
this.pos = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns `true` if the tail is empty (end of string).
|
||||||
|
*/
|
||||||
|
Scanner.prototype.eos = function () {
|
||||||
|
return this.tail === "";
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to match the given regular expression at the current position.
|
||||||
|
* Returns the matched text if it can match, the empty string otherwise.
|
||||||
|
*/
|
||||||
|
Scanner.prototype.scan = function (re) {
|
||||||
|
var match = this.tail.match(re);
|
||||||
|
|
||||||
|
if (match && match.index === 0) {
|
||||||
|
this.tail = this.tail.substring(match[0].length);
|
||||||
|
this.pos += match[0].length;
|
||||||
|
return match[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Skips all text until the given regular expression can be matched. Returns
|
||||||
|
* the skipped string, which is the entire tail if no match can be made.
|
||||||
|
*/
|
||||||
|
Scanner.prototype.scanUntil = function (re) {
|
||||||
|
var match, pos = this.tail.search(re);
|
||||||
|
|
||||||
|
switch (pos) {
|
||||||
|
case -1:
|
||||||
|
match = this.tail;
|
||||||
|
this.pos += this.tail.length;
|
||||||
|
this.tail = "";
|
||||||
|
break;
|
||||||
|
case 0:
|
||||||
|
match = "";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
match = this.tail.substring(0, pos);
|
||||||
|
this.tail = this.tail.substring(pos);
|
||||||
|
this.pos += pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
return match;
|
||||||
|
};
|
||||||
|
|
||||||
|
function Context(view, parent) {
|
||||||
|
this.view = view;
|
||||||
|
this.parent = parent;
|
||||||
|
this.clearCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
Context.make = function (view) {
|
||||||
|
return (view instanceof Context) ? view : new Context(view);
|
||||||
|
};
|
||||||
|
|
||||||
|
Context.prototype.clearCache = function () {
|
||||||
|
this._cache = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
Context.prototype.push = function (view) {
|
||||||
|
return new Context(view, this);
|
||||||
|
};
|
||||||
|
|
||||||
|
Context.prototype.lookup = function (name) {
|
||||||
|
var value = this._cache[name];
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
if (name === ".") {
|
||||||
|
value = this.view;
|
||||||
|
} else {
|
||||||
|
var context = this;
|
||||||
|
|
||||||
|
while (context) {
|
||||||
|
if (name.indexOf(".") > 0) {
|
||||||
|
var names = name.split("."), i = 0;
|
||||||
|
|
||||||
|
value = context.view;
|
||||||
|
|
||||||
|
while (value && i < names.length) {
|
||||||
|
value = value[names[i++]];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
value = context.view[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value != null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
context = context.parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._cache[name] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === "function") {
|
||||||
|
value = value.call(this.view);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
function Renderer() {
|
||||||
|
this.clearCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
Renderer.prototype.clearCache = function () {
|
||||||
|
this._cache = {};
|
||||||
|
this._partialCache = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
Renderer.prototype.compile = function (tokens, tags) {
|
||||||
|
if (typeof tokens === "string") {
|
||||||
|
tokens = parse(tokens, tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
var fn = compileTokens(tokens),
|
||||||
|
self = this;
|
||||||
|
|
||||||
|
return function (view) {
|
||||||
|
return fn(Context.make(view), self);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
Renderer.prototype.compilePartial = function (name, tokens, tags) {
|
||||||
|
this._partialCache[name] = this.compile(tokens, tags);
|
||||||
|
return this._partialCache[name];
|
||||||
|
};
|
||||||
|
|
||||||
|
Renderer.prototype.render = function (template, view) {
|
||||||
|
var fn = this._cache[template];
|
||||||
|
|
||||||
|
if (!fn) {
|
||||||
|
fn = this.compile(template);
|
||||||
|
this._cache[template] = fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fn(view);
|
||||||
|
};
|
||||||
|
|
||||||
|
Renderer.prototype._section = function (name, context, callback) {
|
||||||
|
var value = context.lookup(name);
|
||||||
|
|
||||||
|
switch (typeof value) {
|
||||||
|
case "object":
|
||||||
|
if (isArray(value)) {
|
||||||
|
var buffer = "";
|
||||||
|
|
||||||
|
for (var i = 0, len = value.length; i < len; ++i) {
|
||||||
|
buffer += callback(context.push(value[i]), this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value ? callback(context.push(value), this) : "";
|
||||||
|
case "function":
|
||||||
|
// TODO: The text should be passed to the callback plain, not rendered.
|
||||||
|
var sectionText = callback(context, this),
|
||||||
|
self = this;
|
||||||
|
|
||||||
|
var scopedRender = function (template) {
|
||||||
|
return self.render(template, context);
|
||||||
|
};
|
||||||
|
|
||||||
|
return value.call(context.view, sectionText, scopedRender) || "";
|
||||||
|
default:
|
||||||
|
if (value) {
|
||||||
|
return callback(context, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
Renderer.prototype._inverted = function (name, context, callback) {
|
||||||
|
var value = context.lookup(name);
|
||||||
|
|
||||||
|
// From the spec: inverted sections may render text once based on the
|
||||||
|
// inverse value of the key. That is, they will be rendered if the key
|
||||||
|
// doesn't exist, is false, or is an empty list.
|
||||||
|
if (value == null || value === false || (isArray(value) && value.length === 0)) {
|
||||||
|
return callback(context, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
Renderer.prototype._partial = function (name, context) {
|
||||||
|
var fn = this._partialCache[name];
|
||||||
|
|
||||||
|
if (fn) {
|
||||||
|
return fn(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
Renderer.prototype._name = function (name, context, escape) {
|
||||||
|
var value = context.lookup(name);
|
||||||
|
|
||||||
|
if (typeof value === "function") {
|
||||||
|
value = value.call(context.view);
|
||||||
|
}
|
||||||
|
|
||||||
|
var string = (value == null) ? "" : String(value);
|
||||||
|
|
||||||
|
if (escape) {
|
||||||
|
return escapeHtml(string);
|
||||||
|
}
|
||||||
|
|
||||||
|
return string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Low-level function that compiles the given `tokens` into a
|
||||||
|
* function that accepts two arguments: a Context and a
|
||||||
|
* Renderer. Returns the body of the function as a string if
|
||||||
|
* `returnBody` is true.
|
||||||
|
*/
|
||||||
|
function compileTokens(tokens, returnBody) {
|
||||||
|
var body = ['""'];
|
||||||
|
var token, method, escape;
|
||||||
|
|
||||||
|
for (var i = 0, len = tokens.length; i < len; ++i) {
|
||||||
|
token = tokens[i];
|
||||||
|
|
||||||
|
switch (token.type) {
|
||||||
|
case "#":
|
||||||
|
case "^":
|
||||||
|
method = (token.type === "#") ? "_section" : "_inverted";
|
||||||
|
body.push("r." + method + "(" + quote(token.value) + ", c, function (c, r) {\n" +
|
||||||
|
" " + compileTokens(token.tokens, true) + "\n" +
|
||||||
|
"})");
|
||||||
|
break;
|
||||||
|
case "{":
|
||||||
|
case "&":
|
||||||
|
case "name":
|
||||||
|
escape = token.type === "name" ? "true" : "false";
|
||||||
|
body.push("r._name(" + quote(token.value) + ", c, " + escape + ")");
|
||||||
|
break;
|
||||||
|
case ">":
|
||||||
|
body.push("r._partial(" + quote(token.value) + ", c)");
|
||||||
|
break;
|
||||||
|
case "text":
|
||||||
|
body.push(quote(token.value));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to a string body.
|
||||||
|
body = "return " + body.join(" + ") + ";";
|
||||||
|
|
||||||
|
// Good for debugging.
|
||||||
|
// console.log(body);
|
||||||
|
|
||||||
|
if (returnBody) {
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For great evil!
|
||||||
|
return new Function("c, r", body);
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeTags(tags) {
|
||||||
|
if (tags.length === 2) {
|
||||||
|
return [
|
||||||
|
new RegExp(escapeRe(tags[0]) + "\\s*"),
|
||||||
|
new RegExp("\\s*" + escapeRe(tags[1]))
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Invalid tags: " + tags.join(" "));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forms the given linear array of `tokens` into a nested tree structure
|
||||||
|
* where tokens that represent a section have a "tokens" array property
|
||||||
|
* that contains all tokens that are in that section.
|
||||||
|
*/
|
||||||
|
function nestTokens(tokens) {
|
||||||
|
var tree = [];
|
||||||
|
var collector = tree;
|
||||||
|
var sections = [];
|
||||||
|
var token, section;
|
||||||
|
|
||||||
|
for (var i = 0; i < tokens.length; ++i) {
|
||||||
|
token = tokens[i];
|
||||||
|
|
||||||
|
switch (token.type) {
|
||||||
|
case "#":
|
||||||
|
case "^":
|
||||||
|
token.tokens = [];
|
||||||
|
sections.push(token);
|
||||||
|
collector.push(token);
|
||||||
|
collector = token.tokens;
|
||||||
|
break;
|
||||||
|
case "/":
|
||||||
|
if (sections.length === 0) {
|
||||||
|
throw new Error("Unopened section: " + token.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
section = sections.pop();
|
||||||
|
|
||||||
|
if (section.value !== token.value) {
|
||||||
|
throw new Error("Unclosed section: " + section.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sections.length > 0) {
|
||||||
|
collector = sections[sections.length - 1].tokens;
|
||||||
|
} else {
|
||||||
|
collector = tree;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
collector.push(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure there were no open sections when we're done.
|
||||||
|
section = sections.pop();
|
||||||
|
|
||||||
|
if (section) {
|
||||||
|
throw new Error("Unclosed section: " + section.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tree;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combines the values of consecutive text tokens in the given `tokens` array
|
||||||
|
* to a single token.
|
||||||
|
*/
|
||||||
|
function squashTokens(tokens) {
|
||||||
|
var lastToken;
|
||||||
|
|
||||||
|
for (var i = 0; i < tokens.length; ++i) {
|
||||||
|
var token = tokens[i];
|
||||||
|
|
||||||
|
if (lastToken && lastToken.type === "text" && token.type === "text") {
|
||||||
|
lastToken.value += token.value;
|
||||||
|
tokens.splice(i--, 1); // Remove this token from the array.
|
||||||
|
} else {
|
||||||
|
lastToken = token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Breaks up the given `template` string into a tree of token objects. If
|
||||||
|
* `tags` is given here it must be an array with two string values: the
|
||||||
|
* opening and closing tags used in the template (e.g. ["<%", "%>"]). Of
|
||||||
|
* course, the default is to use mustaches (i.e. Mustache.tags).
|
||||||
|
*/
|
||||||
|
function parse(template, tags) {
|
||||||
|
tags = tags || exports.tags;
|
||||||
|
|
||||||
|
var tagRes = escapeTags(tags);
|
||||||
|
var scanner = new Scanner(template);
|
||||||
|
|
||||||
|
var tokens = [], // Buffer to hold the tokens
|
||||||
|
spaces = [], // Indices of whitespace tokens on the current line
|
||||||
|
hasTag = false, // Is there a {{tag}} on the current line?
|
||||||
|
nonSpace = false; // Is there a non-space char on the current line?
|
||||||
|
|
||||||
|
// Strips all whitespace tokens array for the current line
|
||||||
|
// if there was a {{#tag}} on it and otherwise only space.
|
||||||
|
var stripSpace = function () {
|
||||||
|
if (hasTag && !nonSpace) {
|
||||||
|
while (spaces.length) {
|
||||||
|
tokens.splice(spaces.pop(), 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
spaces = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
hasTag = false;
|
||||||
|
nonSpace = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
var type, value, chr;
|
||||||
|
|
||||||
|
while (!scanner.eos()) {
|
||||||
|
value = scanner.scanUntil(tagRes[0]);
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
for (var i = 0, len = value.length; i < len; ++i) {
|
||||||
|
chr = value.charAt(i);
|
||||||
|
|
||||||
|
if (isWhitespace(chr)) {
|
||||||
|
spaces.push(tokens.length);
|
||||||
|
} else {
|
||||||
|
nonSpace = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens.push({type: "text", value: chr});
|
||||||
|
|
||||||
|
if (chr === "\n") {
|
||||||
|
stripSpace(); // Check for whitespace on the current line.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match the opening tag.
|
||||||
|
if (!scanner.scan(tagRes[0])) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasTag = true;
|
||||||
|
type = scanner.scan(tagRe) || "name";
|
||||||
|
|
||||||
|
// Skip any whitespace between tag and value.
|
||||||
|
scanner.scan(whiteRe);
|
||||||
|
|
||||||
|
// Extract the tag value.
|
||||||
|
if (type === "=") {
|
||||||
|
value = scanner.scanUntil(eqRe);
|
||||||
|
scanner.scan(eqRe);
|
||||||
|
scanner.scanUntil(tagRes[1]);
|
||||||
|
} else if (type === "{") {
|
||||||
|
var closeRe = new RegExp("\\s*" + escapeRe("}" + tags[1]));
|
||||||
|
value = scanner.scanUntil(closeRe);
|
||||||
|
scanner.scan(curlyRe);
|
||||||
|
scanner.scanUntil(tagRes[1]);
|
||||||
|
} else {
|
||||||
|
value = scanner.scanUntil(tagRes[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match the closing tag.
|
||||||
|
if (!scanner.scan(tagRes[1])) {
|
||||||
|
throw new Error("Unclosed tag at " + scanner.pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens.push({type: type, value: value});
|
||||||
|
|
||||||
|
if (type === "name" || type === "{" || type === "&") {
|
||||||
|
nonSpace = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the tags for the next time around.
|
||||||
|
if (type === "=") {
|
||||||
|
tags = value.split(spaceRe);
|
||||||
|
tagRes = escapeTags(tags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
squashTokens(tokens);
|
||||||
|
|
||||||
|
return nestTokens(tokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The high-level clearCache, compile, compilePartial, and render functions
|
||||||
|
// use this default renderer.
|
||||||
|
var _renderer = new Renderer();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all cached templates and partials.
|
||||||
|
*/
|
||||||
|
function clearCache() {
|
||||||
|
_renderer.clearCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* High-level API for compiling the given `tokens` down to a reusable
|
||||||
|
* function. If `tokens` is a string it will be parsed using the given `tags`
|
||||||
|
* before it is compiled.
|
||||||
|
*/
|
||||||
|
function compile(tokens, tags) {
|
||||||
|
return _renderer.compile(tokens, tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* High-level API for compiling the `tokens` for the partial with the given
|
||||||
|
* `name` down to a reusable function. If `tokens` is a string it will be
|
||||||
|
* parsed using the given `tags` before it is compiled.
|
||||||
|
*/
|
||||||
|
function compilePartial(name, tokens, tags) {
|
||||||
|
return _renderer.compilePartial(name, tokens, tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* High-level API for rendering the `template` using the given `view`. The
|
||||||
|
* optional `partials` object may be given here for convenience, but note that
|
||||||
|
* it will cause all partials to be re-compiled, thus hurting performance. Of
|
||||||
|
* course, this only matters if you're going to render the same template more
|
||||||
|
* than once. If so, it is best to call `compilePartial` before calling this
|
||||||
|
* function and to leave the `partials` argument blank.
|
||||||
|
*/
|
||||||
|
function render(template, view, partials) {
|
||||||
|
if (partials) {
|
||||||
|
for (var name in partials) {
|
||||||
|
compilePartial(name, partials[name]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _renderer.render(template, view);
|
||||||
|
}
|
||||||
|
|
||||||
|
return exports;
|
||||||
|
}())));
|
Loading…
Reference in New Issue