1
0
Fork 0

Tracks import

This commit is contained in:
Gregory Eremin 2012-09-01 21:55:01 +04:00
parent b5f616a9d9
commit 43422624f1
34 changed files with 1094 additions and 156 deletions

View File

@ -11,12 +11,13 @@ group :assets do
gem "hogan_assets"
gem "uglifier", ">= 1.0.3"
gem "bourbon"
end
gem "jquery-rails"
# To use Jbuilder templates for JSON
# gem "jbuilder"
gem "jbuilder"
gem "thin"

View File

@ -44,7 +44,10 @@ GEM
i18n (~> 0.6)
multi_json (~> 1.0)
arel (3.0.2)
blankslate (2.1.2.4)
bootstrap-sass (2.0.4.0)
bourbon (2.1.1)
sass (>= 3.1)
builder (3.0.0)
capistrano (2.12.0)
highline
@ -80,6 +83,9 @@ GEM
multi_json (~> 1.0)
multi_xml
i18n (0.6.0)
jbuilder (0.4.0)
activesupport (>= 3.0.0)
blankslate (>= 2.1.2.4)
journey (1.0.4)
jquery-rails (2.1.1)
railties (>= 3.1.0, < 5.0)
@ -168,10 +174,12 @@ PLATFORMS
DEPENDENCIES
beatparser!
bootstrap-sass (~> 2.0.4.0)
bourbon
capistrano
coffee-rails (~> 3.2.1)
eco
hogan_assets
jbuilder
jquery-rails
pg
rails (= 3.2.8)

View File

@ -12,6 +12,7 @@
//
//= require jquery
//= require jquery_ujs
//= require jquery.autocomplete
//= require mustache
//= require hogan
//= require underscore
@ -20,3 +21,4 @@
//= require backbone_datalink
//= require backbone/beat_haven
//= require_tree .

View File

@ -0,0 +1,12 @@
$ ->
$(".navbar-search input").focus ->
$(this).animate(width: 249)
$(".player").animate(width: 408)
$(".navbar-search input").blur ->
$(this).animate(width: 99)
$(".player").animate(width: 558)
window.desired = $(".navbar-search input").autocomplete
serviceUrl: "/api/search/complete"
onSelect: (selected) ->
Backbone.history.navigate("/search/"+selected.replace(/\s/g, "+"), true)

View File

@ -9,10 +9,24 @@ window.BeatHaven =
Collections: {}
Routers: {}
Views: {}
player: null
init: ->
new BeatHaven.Routers.Artist()
Backbone.history.start();
new BeatHaven.Routers.Album()
new BeatHaven.Routers.Search()
@player = new BeatHaven.Models.Player()
Backbone.history.start(pushState: true);
$("a").live "click", (e) ->
if $(this).attr("href").substr(0, 1) == "/"
e.preventDefault()
Backbone.history.navigate($(this).attr("href"), true)
return false
else
alert "Window close attempt!"
return false
true
$ ->
BeatHaven.init()

View File

@ -0,0 +1 @@
class BeatHaven.Collections.Tracklist extends Backbone.Collection

View File

@ -0,0 +1,2 @@
class BeatHaven.Models.Album extends Backbone.Model
urlRoot: "/api/albums"

View File

@ -0,0 +1,11 @@
class BeatHaven.Models.Player extends Backbone.Model
playlist_on: false
playlist: null
tracks: null
initialize: ->
@playlist = new BeatHaven.Collections.Tracklist()
@tracks = new BeatHaven.Collections.Tracklist()
update_title: (params) ->
$(".player .progress-bar .title").html("#{params.artists.join(', ')} &mdash; #{params.track}")

View File

@ -0,0 +1,7 @@
class BeatHaven.Models.Track extends Backbone.Model
play: ->
BeatHaven.player.update_title(
artists: @.get("artists")
track: @.get("title")
)

View File

@ -0,0 +1,9 @@
class BeatHaven.Routers.Album extends Backbone.Router
routes:
"album/:id": "show"
show: (id) ->
album = new BeatHaven.Models.Album(id: id)
album.fetch()
view = new BeatHaven.Views.AlbumShow(model: album)
$("#main").html(view.render().el)

View File

@ -0,0 +1,16 @@
class BeatHaven.Routers.Search extends Backbone.Router
routes:
"search/:query": "search"
search: (query) ->
$(".navbar-search .search-query").attr("disabled", "disabled").blur()
$.ajax(
url: "/api/search/wtfis?q=#{query}"
success: (data) ->
if data.found?
Backbone.history.navigate(data.found, true)
$(".navbar-search .search-query").val("").removeAttr("disabled")
else
alert "Not found! :("
$(".navbar-search .search-query").removeAttr("disabled").focus()
)

View File

@ -0,0 +1,28 @@
<div class="artist-page">
<h1 class="title">{{album_title}} ({{album_year}})</h1>
<div class="album">
<div class="pic">
<img src="{{album_pic}}" alt="{{album_title}}"><br>
<a href="" class="btn btn-bh-blue album-add"><i class="icon-plus icon-white"></i> Add to playlist</a>
<a href="" class="btn btn-bh-green album-play"><i class="icon-play icon-white"></i> Play</a>
</div>
<ul class="tracks">
{{#album_tracks}}
<li data-id="{{track_id}}">
<a href="" class="btn btn-round track-play"><i class="icon-play"></i></a>
<div class="title">
<div class="inner">
<a href="{{track_url}}" class="track-link">{{track_title}}</a>
<span class="delimiter">&mdash;</span>
<ul class="artists">
{{#track_artists}}<li><a href="{{artist_url}}" class="artist-page">{{artist_title}}</a></li>{{/track_artists}}
</ul>
</div>
</div>
<span class="length">{{track_length}}</span>
<a href="" class="btn btn-round track-add"><i class="icon-plus"></i></a>
</li>
{{/album_tracks}}
</ul>
</div>
</div>

View File

@ -0,0 +1,39 @@
<div class="artist-page">
<h1>{{artist_title}}</h1>
<div class="artist-info">
<div class="pic">
<img src="{{artist_pic}}" alt="{{artist_title}}">
</div>
<div class="bio">{{& artist_bio}}</div>
</div>
<div class="albums">
{{#artist_albums}}
<div class="album">
<div class="pic">
<img src="{{album_pic}}" alt="{{album_title}}"><br>
<div class="bh-underlay-blue">
<a href="" class="btn btn-bh-dark album-add"><i class="icon-plus icon-white"></i> Add to playlist</a>
</div>
<div class="bh-underlay-green">
<a href="" class="btn btn-bh-dark album-play"><i class="icon-play icon-white"></i> Play</a>
</div>
</div>
<h2 class="title">{{album_title}} ({{album_year}})</h2>
<ul class="tracks">
{{#album_tracks}}
<li data-id="{{track_id}}">
<a href="" class="btn btn-round track-play"><i class="icon-play"></i></a>
<div class="title">
<div class="inner">
<a href="{{track_url}}" class="track-link">{{track_title}}</a>
</div>
</div>
<span class="length">{{track_length}}</span>
<a href="" class="btn btn-round track-add"><i class="icon-plus"></i></a>
</li>
{{/album_tracks}}
</ul>
</div>
{{/artist_albums}}
</div>
</div>

View File

@ -1,39 +0,0 @@
<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>

View File

@ -0,0 +1,13 @@
class BeatHaven.Views.AlbumShow extends Backbone.View
template: HoganTemplates["backbone/templates/album/show"]
initialize: ->
@model.on("change", @render, this)
render: ->
if typeof @model.get("album_tracks") != "undefined"
for track_info in @model.get("album_tracks")
track = new BeatHaven.Models.Track(track_info.meta)
BeatHaven.player.tracks.push(track)
$(@el).html(@template.render(@model.toJSON()))
this

View File

@ -1,10 +1,14 @@
class BeatHaven.Views.ArtistShow extends Backbone.View
template: HoganTemplates["backbone/templates/artists/show"]
template: HoganTemplates["backbone/templates/artist/show"]
initialize: ->
@model.on("change", @render, this)
render: ->
return this if typeof @model.attributes.id is "string"
$(@el).html(@template.render(@model.toJSON()))
if typeof @model.get("artist_bio") != "undefined"
for album_info in @model.get("artist_albums")
for track_info in album_info.album_tracks
track = new BeatHaven.Models.Track(track_info.meta)
BeatHaven.player.tracks.push(track)
$(@el).html(@template.render(@model.toJSON()))
this

View File

@ -0,0 +1,126 @@
.artist-page .album .tracks {
margin-left: 265px;
& > li {
position: relative;
display: block;
width: 100%;
height: 38px;
list-style: none;
font-family: "Source Sans Pro", $helvetica;
font-size: 20px;
line-height: 28px;
text-shadow: 0 0 1px rgba(0, 0, 0, .3);
&:after {
content: "";
display: block;
width: 627px;
float: right;
border-bottom: #e0e0e0 1px solid;
margin: 0 5px 0 0;
}
&:first-child:after, &:hover:after {
display: none;
}
.btn-round {
width: 10px;
height: 22px;
border-radius: 50px;
i {
margin: 3px 0 0 -1px;
}
}
.track-play {
position: absolute;
top: 3px;
left: 5px;
i {
opacity: .3;
}
}
// Track title
.title {
position: absolute;
top: 5px;
left: 42px;
height: 28px;
max-width: 580px;
overflow: hidden;
.inner {
width: 1000px;
.track-link {
color: #303030;
}
.artists {
@include inline-block;
margin: 0;
padding: 0;
li {
@include inline-block;
margin: 0;
padding: 0;
color: #808080;
.artist-page {
color: #808080;
}
.delimiter {
color: #808080;
}
&:before {
content: ", ";
}
&:first-child:before {
content: "";
}
}
}
}
}
.length {
position: absolute;
top: 5px;
right: 5px;
}
.track-add {
position: absolute;
top: 3px;
right: 5px;
display: none;
}
// li:hover
&:hover{
border: {
width: 0px;
style: solid;
color: transparent;
radius: 4px;
}
background-color: rgba(200, 200, 200, .3);
.track-play i {
opacity: 1;
}
.track-add {
display: block;
}
.length {
display: none;
}
}
}
}

File diff suppressed because one or more lines are too long

View File

@ -1,27 +1,28 @@
.artist-page {
h1 {
font-family: Lobster, Georgia, serif;
font-family: "Lobster Two", Georgia, serif;
font-weight: 700;
font-size: 42px;
line-height: 64px;
letter-spacing: 1px;
text-shadow: 1px 1px 0 rgba(0, 0, 0, .2);
text-shadow: 0 0 1px rgba(0, 0, 0, .3);
}
.artist-info {
min-height: 250px;
margin-bottom: 50px;
margin-bottom: 20px;
.pic {
float: left;
img {
width: 250px;
height: 250px;
margin-top: 5px;
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);
@include box-shadow(0 0 3px 1px hsla(0, 0%, 0%, 0.3));
}
}
.bio {
@ -34,13 +35,14 @@
}
.album {
min-height: 250px;
min-height: 280px;
margin-bottom: 30px;
h2 {
font-family: Lobster, Georgia, Serif;
font-family: "Lobster Two", Georgia, Serif;
font-weight: 700;
font-size: 26px;
text-shadow: 1px 1px 0 rgba(0, 0, 0, .2);
text-shadow: 0 0 1px rgba(0, 0, 0, .3);
}
.pic {
float: left;
@ -51,82 +53,18 @@
background-color: #a0a0a0;
width: 250px;
height: 250px;
margin-top: 5px;
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);
@include box-shadow(0 0 3px 1px hsla(0, 0%, 0%, 0.3));
margin-bottom: 10px;
}
}
.title {
& > .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;
}
}
}
}
}

View File

@ -0,0 +1,39 @@
.autocomplete-w1 {
margin: 10px 0 0 0;
width: 268px;
background: {
color: #f0f0f0;
color: rgba(240, 243, 246, .95);
}
@include box-shadow(5px 14px 5px rgba(0, 0, 0, .15));
.autocomplete {
div {
width: 258px;
padding: 5px 5px;
border: {
color: transparent;
style: solid;
width: 1px;
}
background: {
color: #f0f0f0;
color: rgba(240, 243, 246, .95);
}
&:last-child {
border-bottom: {
left-radius: 5px;
right-radius: 5px;
}
}
&.selected {
color: #ffffff;
background-color: #49AFCD;
background-color: rgba(73, 175, 205, .95);
border-radius: 5px;
}
}
}
}

View File

@ -0,0 +1,28 @@
.bh-underlay-blue, .bh-underlay-green {
@include inline-block;
border-color: transparent;
@include border-radius(4px);
}
.bh-underlay-blue {
background-color: #126be8;
background: url($light_blue_noise);
}
.bh-underlay-green {
background-color: #17ed25;
background: url($light_green_noise);
}
.btn-bh-dark, .btn-bh-light {
background-position: 0 0 !important;
color: #fafafa;
text-shadow: 0 1px 1px rgba(0, 0, 0, .35);
&:hover {
color: #ffffff;
}
}
.btn-bh-dark {
@include buttonBackground(rgba(0, 0, 0, .05), rgba(0, 0, 0, .15));
}
.btn-bh-light {
@include buttonBackground(rgba(255, 255, 255, .05), rgba(255, 255, 255, .15));
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,51 @@
.player {
width: 558px;
.controls {
float: left;
margin: 3px 0 0 -103px;
a {
margin: 5px 6px 0 -9px;
}
}
.progress-bar {
position: relative;
width: 100%;
height: 26px;
margin: 8px 0 0;
background-color: rgba(0, 0, 0, .3);
border: {
style: solid;
width: 1px;
color: rgba(0, 0, 0, .3);
radius: 4px;
}
@include box-shadow(inset 0 0 1px rgba(0, 0, 0, .2));
.bar {
position: absolute;
height: 26px;
background-color: rgba(255, 255, 255, .1);
@include box-shadow(inset 0 0 2px rgba(255, 255, 255, .2));
@include border-radius(4px 0px 0px 4px);
}
.title {
position: absolute;
left: 8px;
font: {
family: "Source Sans Pro", $helvetica;
size: 14px;
}
line-height: 24px;
color: #ffffff;
text-shadow: 1px 1px 0 rgba(80, 80, 80, .4);
}
.move-it {
position: absolute;
width: 100%;
height: 26px;
z-index: 9999;
}
}
}

View File

@ -4,5 +4,12 @@ module Api
album = Album.find(params[:id])
redirect_to album.load_pic
end
def show
album = Album.find(params[:id])
return render json: { fail: true } if album.nil?
render json: album.dump_json
end
end
end

View File

@ -0,0 +1,35 @@
module Api
class SearchController < ApplicationController
def complete
return render json: { suggestions: [] } if params[:query].to_s.length == 0
suggestions = (Robbie::Autocomplete.complete(params[:query].to_s) || [])
render json: {
query: params[:query],
suggestions: suggestions
}
end
def wtfis
result = (Robbie::Autocomplete.search(params[:q].to_s) || []).first
unless result.nil?
if result.instance_of? Robbie::Artist
artist = Artist.find_or_create_by_rovi_id(result.id)
puts artist.inspect
unless artist.name?
artist.import
end
return render json: { found: "/artist/#{result.name.gsub(" ", "+")}" }
elsif result.instance_of? Robbie::Album
album = Album.find_or_create_by_rovi_id(result.id)
unless album.title?
album.import
end
return render json: { found: "/album/#{album.id}" }
end
end
render json: { found: nil }
end
end
end

View File

@ -2,7 +2,18 @@ class Album < ActiveRecord::Base
belongs_to :artist
has_many :tracks
attr_accessible :artist_id, :pic, :rovi_id, :title, :year
scope :shown, lambda {
self
.where('"albums"."year" > ?', 0)
.where(is_hidden: false)
.joins(:tracks)
.group('"albums"."id"')
.having('count("tracks"."id") > ?', 0)
.order('"albums"."year" ASC')
}
attr_accessible :artist_id, :pic, :rovi_id, :title, :year, :is_hidden
VA = "Various Artists"
def pic_safe
unless pic.nil?
@ -13,7 +24,7 @@ class Album < ActiveRecord::Base
end
def load_pic
info = BeatParser::Sources::Lastfm.album_info(artist.name, title)
info = BeatParser::Sources::Lastfm.album_info((artist.nil? ? VA : artist.name), title)
unless info[:pic].nil?
update_attributes(pic: info[:pic])
info[:pic]
@ -21,4 +32,65 @@ class Album < ActiveRecord::Base
"/assets/images/album-dummy.png"
end
end
def dump_json
Jbuilder.encode do |j|
j.album_title title
j.album_year year
j.album_pic pic_safe
j.album_tracks tracks.to_a do |j, track|
j.track_id track.id
j.track_title track.title
j.track_duration track.duration
j.track_disc track.disc_id
j.track_position track.position
j.meta do |j|
j.id track.id
j.title track.title
j.duration track.duration
j.length track.length
j.artists track.artists.map(&:name)
j.album title
j.album_pic pic_safe
end
end
end
end
def import
return unless rovi_id?
Album.import(Robbie::Album.find(rovi_id))
end
class << self
def import(rovi_album)
data = BeatParser::Aggregator.new.album(rovi_album.id)
album = Album.find_or_create_by_rovi_id(data[:id])
album.update_attributes(
title: data[:title],
year: data[:year].to_i
)
data[: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
album
end
end
end

View File

@ -11,21 +11,39 @@ class Artist < ActiveRecord::Base
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],
)
Jbuilder.encode do |j|
j.artist_title name
j.artist_pic pic
j.artist_bio bio
j.artist_loaded loaded?
j.artist_albums albums.shown.to_a do |j, album|
j.album_title album.title
j.album_year album.year
j.album_pic album.pic_safe
j.album_tracks album.tracks.to_a do |j, track|
j.track_id track.id
j.track_title track.title
j.track_duration track.duration
j.track_disc track.disc_id
j.track_position track.position
j.meta do |j|
j.id track.id
j.title track.title
j.duration track.duration
j.length track.length
j.artists track.artists.map(&:name)
j.album album.title
j.album_pic album.pic_safe
end
end
end
end
end
def import
return unless rovi_id?
Artist.import(Robbie::Artist.find(rovi_id))
end
class << self
@ -43,7 +61,7 @@ class Artist < ActiveRecord::Base
end
def import(rovi_artist)
data = BeatParser::Aggregator.new.combine(rovi_artist.id)
data = BeatParser::Aggregator.new.artist(rovi_artist.id)
artist = Artist.find_or_create_by_rovi_id(data[:id])
artist.update_attributes(
name: data[:name],
@ -83,6 +101,7 @@ class Artist < ActiveRecord::Base
)
ArtistGenre.find_or_create_by_artist_id_and_genre_id(artist.id, genre.id)
end
artist
end
end
end

View File

@ -12,10 +12,25 @@
<div class="navbar-inner">
<div class="container">
<a class="brand" href="">BeatHaven</a>
<form class="navbar-search pull-left">
<input type="search" placeholder="Search" class="search-query">
</form>
<div class="player pull-right">
<div class="controls">
<a href="" class="btn btn-bh-dark prev"><i class="icon-backward icon-white"></i></a>
<a href="" class="btn btn-bh-dark pause"><i class="icon-pause icon-white"></i></a>
<a href="" class="btn btn-bh-dark next"><i class="icon-forward icon-white"></i></a>
</div>
<div class="progress-bar">
<div class="bar" style="width: 42%"></div>
<div class="title">Foo Fighters &mdash; Alone + Easy Target</div>
<div class="move-it"></div>
</div>
</div>
</div>
</div>
</div>
<div class="container" id="main">Loading...</div>
<div class="container" id="main">You can't steal what's free</div>
</body>
</html>

View File

@ -3,5 +3,7 @@ lastfm:
api_secret: secret
client_name: BeatHaven
rovi:
api_key: secret
api_secret: secret
meta_api_key: secret
meta_api_secret: secret
autocomplete_api_key: secret
autocomplete_api_secret: secret

View File

@ -5,9 +5,6 @@ File.open("#{Rails.root}/config/api_keys.yml") do |file|
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.setup(config["rovi"].symbolize_keys)
Robbie.enable_cache
end

View File

@ -1,10 +1,14 @@
BeatHaven::Application.routes.draw do
namespace :api do
resources :artists, only: [:show], constraints: { id: /.+/ }
resources :albums, only: [:picture] do
resources :albums, only: [:show, :picture] do
member { get :picture }
end
resources :search, only: [] do
collection { get :complete; get :wtfis }
end
end
match "/:path" => "application#main", constraints: { path: /.*/ }
root to: "application#main"
end

View File

@ -0,0 +1,5 @@
class AddIsHiddenFieldToAlbums < ActiveRecord::Migration
def change
add_column :albums, :is_hidden, :boolean, default: false
end
end

View File

@ -11,7 +11,7 @@
#
# It's strongly recommended to check this file into your version control system.
ActiveRecord::Schema.define(:version => 20120826172120) do
ActiveRecord::Schema.define(:version => 20120901111329) do
create_table "albums", :force => true do |t|
t.integer "artist_id"
@ -19,8 +19,9 @@ ActiveRecord::Schema.define(:version => 20120826172120) do
t.string "title"
t.integer "year"
t.string "pic"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
t.boolean "is_hidden", :default => false
end
create_table "artist_genres", :force => true do |t|

View File

@ -0,0 +1,433 @@
/**
* Ajax Autocomplete for jQuery, version 1.1.5
* (c) 2010 Tomas Kirda, Vytautas Pranskunas
*
* Ajax Autocomplete for jQuery is freely distributable under the terms of an MIT-style license.
* For details, see the web site: http://www.devbridge.com/projects/autocomplete/jquery/
*
* Last Review: 07/24/2012
*/
/*jslint onevar: true, evil: true, nomen: true, eqeqeq: true, bitwise: true, regexp: true, newcap: true, immed: true */
/*global window: true, document: true, clearInterval: true, setInterval: true, jQuery: true */
(function ($) {
var reEscape = new RegExp('(\\' + ['/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\'].join('|\\') + ')', 'g');
function fnFormatResult(value, data, currentValue) {
var pattern = '(' + currentValue.replace(reEscape, '\\$1') + ')';
return value.replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
}
function Autocomplete(el, options) {
this.el = $(el);
this.el.attr('autocomplete', 'off');
this.suggestions = [];
this.data = [];
this.badQueries = [];
this.selectedIndex = -1;
this.currentValue = this.el.val();
this.intervalId = 0;
this.cachedResponse = [];
this.onChangeInterval = null;
this.onChange = null;
this.ignoreValueChange = false;
this.serviceUrl = options.serviceUrl;
this.isLocal = false;
this.options = {
autoSubmit: false,
minChars: 1,
maxHeight: 300,
deferRequestBy: 0,
width: 0,
highlight: true,
params: {},
fnFormatResult: fnFormatResult,
delimiter: null,
zIndex: 9999
};
this.initialize();
this.setOptions(options);
this.el.data('autocomplete', this);
}
$.fn.autocomplete = function (options, optionName) {
var autocompleteControl;
if (typeof options == 'string') {
autocompleteControl = this.data('autocomplete');
if (typeof autocompleteControl[options] == 'function') {
autocompleteControl[options](optionName);
}
} else {
autocompleteControl = new Autocomplete(this.get(0) || $('<input />'), options);
}
return autocompleteControl;
};
Autocomplete.prototype = {
killerFn: null,
initialize: function () {
var me, uid, autocompleteElId;
me = this;
uid = Math.floor(Math.random() * 0x100000).toString(16);
autocompleteElId = 'Autocomplete_' + uid;
this.killerFn = function (e) {
if ($(e.target).parents('.autocomplete').size() === 0) {
me.killSuggestions();
me.disableKillerFn();
}
};
if (!this.options.width) { this.options.width = this.el.width(); }
this.mainContainerId = 'AutocompleteContainter_' + uid;
$('<div id="' + this.mainContainerId + '" style="position:absolute;z-index:9999;"><div class="autocomplete-w1"><div class="autocomplete" id="' + autocompleteElId + '" style="display:none; width:300px;"></div></div></div>').appendTo('body');
this.container = $('#' + autocompleteElId);
this.fixPosition();
if (window.opera) {
this.el.keypress(function (e) { me.onKeyPress(e); });
} else {
this.el.keydown(function (e) { me.onKeyPress(e); });
}
this.el.keyup(function (e) { me.onKeyUp(e); });
this.el.blur(function () { me.enableKillerFn(); });
this.el.focus(function () { me.fixPosition(); });
this.el.change(function () { me.onValueChanged(); });
},
extendOptions: function (options) {
$.extend(this.options, options);
},
setOptions: function (options) {
var o = this.options;
this.extendOptions(options);
if (o.lookup || o.isLocal) {
this.isLocal = true;
if ($.isArray(o.lookup)) { o.lookup = { suggestions: o.lookup, data: [] }; }
}
$('#' + this.mainContainerId).css({ zIndex: o.zIndex });
this.container.css({ maxHeight: o.maxHeight + 'px', width: o.width });
},
clearCache: function () {
this.cachedResponse = [];
this.badQueries = [];
},
disable: function () {
this.disabled = true;
},
enable: function () {
this.disabled = false;
},
fixPosition: function () {
var offset = this.el.offset();
$('#' + this.mainContainerId).css({ top: (offset.top + this.el.innerHeight()) + 'px', left: offset.left + 'px' });
},
enableKillerFn: function () {
var me = this;
$(document).bind('click', me.killerFn);
},
disableKillerFn: function () {
var me = this;
$(document).unbind('click', me.killerFn);
},
killSuggestions: function () {
var me = this;
this.stopKillSuggestions();
this.intervalId = window.setInterval(function () { me.hide(); me.stopKillSuggestions(); }, 300);
},
stopKillSuggestions: function () {
window.clearInterval(this.intervalId);
},
onValueChanged: function () {
this.change(this.selectedIndex);
},
onKeyPress: function (e) {
if (this.disabled || !this.enabled) { return; }
// return will exit the function
// and event will not be prevented
switch (e.keyCode) {
case 27: //KEY_ESC:
this.el.val(this.currentValue);
this.hide();
break;
case 9: //KEY_TAB:
case 13: //KEY_RETURN:
if (this.selectedIndex === -1) {
this.hide();
return;
}
this.select(this.selectedIndex);
if (e.keyCode === 9) { return; }
break;
case 38: //KEY_UP:
this.moveUp();
break;
case 40: //KEY_DOWN:
this.moveDown();
break;
default:
return;
}
e.stopImmediatePropagation();
e.preventDefault();
},
onKeyUp: function (e) {
if (this.disabled) { return; }
switch (e.keyCode) {
case 38: //KEY_UP:
case 40: //KEY_DOWN:
return;
}
clearInterval(this.onChangeInterval);
if (this.currentValue !== this.el.val()) {
if (this.options.deferRequestBy > 0) {
// Defer lookup in case when value changes very quickly:
var me = this;
this.onChangeInterval = setInterval(function () { me.onValueChange(); }, this.options.deferRequestBy);
} else {
this.onValueChange();
}
}
},
onValueChange: function () {
clearInterval(this.onChangeInterval);
this.currentValue = this.el.val();
var q = this.getQuery(this.currentValue);
this.selectedIndex = -1;
if (this.ignoreValueChange) {
this.ignoreValueChange = false;
return;
}
if (q === '' || q.length < this.options.minChars) {
this.hide();
} else {
this.getSuggestions(q);
}
},
getQuery: function (val) {
var d, arr;
d = this.options.delimiter;
if (!d) { return $.trim(val); }
arr = val.split(d);
return $.trim(arr[arr.length - 1]);
},
getSuggestionsLocal: function (q) {
var ret, arr, len, val, i;
arr = this.options.lookup;
len = arr.suggestions.length;
ret = { suggestions: [], data: [] };
q = q.toLowerCase();
for (i = 0; i < len; i++) {
val = arr.suggestions[i];
if (val.toLowerCase().indexOf(q) === 0) {
ret.suggestions.push(val);
ret.data.push(arr.data[i]);
}
}
return ret;
},
getSuggestions: function (q) {
var cr, me;
cr = this.isLocal ? this.getSuggestionsLocal(q) : this.cachedResponse[q]; //dadeta this.options.isLocal ||
if (cr && $.isArray(cr.suggestions)) {
this.suggestions = cr.suggestions;
this.data = cr.data;
this.suggest();
} else if (!this.isBadQuery(q)) {
me = this;
me.options.params.query = q;
$.get(this.serviceUrl, me.options.params, function (txt) { me.processResponse(txt); }, 'text');
}
},
isBadQuery: function (q) {
var i = this.badQueries.length;
while (i--) {
if (q.indexOf(this.badQueries[i]) === 0) { return true; }
}
return false;
},
hide: function () {
this.enabled = false;
this.selectedIndex = -1;
this.container.hide();
},
suggest: function () {
if (this.suggestions.length === 0) {
this.hide();
return;
}
var me, len, div, f, v, i, s, mOver, mClick;
me = this;
len = this.suggestions.length;
f = this.options.fnFormatResult;
v = this.getQuery(this.currentValue);
mOver = function (xi) { return function () { me.activate(xi); }; };
mClick = function (xi) { return function () { me.select(xi); }; };
this.container.hide().empty();
for (i = 0; i < len; i++) {
s = this.suggestions[i];
div = $((me.selectedIndex === i ? '<div class="selected"' : '<div') + ' title="' + s + '">' + f(s, this.data[i], v) + '</div>');
div.mouseover(mOver(i));
div.click(mClick(i));
this.container.append(div);
}
this.enabled = true;
this.container.show();
},
processResponse: function (text) {
var response;
try {
response = eval('(' + text + ')');
} catch (err) { return; }
if (!$.isArray(response.data)) { response.data = []; }
if (!this.options.noCache) {
this.cachedResponse[response.query] = response;
if (response.suggestions.length === 0) { this.badQueries.push(response.query); }
}
if (response.query === this.getQuery(this.currentValue)) {
this.suggestions = response.suggestions;
this.data = response.data;
this.suggest();
}
},
activate: function (index) {
var divs, activeItem;
divs = this.container.children();
// Clear previous selection:
if (this.selectedIndex !== -1 && divs.length > this.selectedIndex) {
$(divs.get(this.selectedIndex)).removeClass();
}
this.selectedIndex = index;
if (this.selectedIndex !== -1 && divs.length > this.selectedIndex) {
activeItem = divs.get(this.selectedIndex);
$(activeItem).addClass('selected');
}
return activeItem;
},
deactivate: function (div, index) {
div.className = '';
if (this.selectedIndex === index) { this.selectedIndex = -1; }
},
select: function (i) {
var selectedValue, f;
selectedValue = this.suggestions[i];
if (selectedValue) {
this.el.val(selectedValue);
if (this.options.autoSubmit) {
f = this.el.parents('form');
if (f.length > 0) { f.get(0).submit(); }
}
this.ignoreValueChange = true;
this.hide();
this.onSelect(i);
}
},
change: function (i) {
var selectedValue, fn, me;
me = this;
selectedValue = this.suggestions[i];
if (selectedValue) {
var s, d;
s = me.suggestions[i];
d = me.data[i];
me.el.val(me.getValue(s));
}
else {
s = '';
d = -1;
}
fn = me.options.onChange;
if ($.isFunction(fn)) { fn(s, d, me.el); }
},
moveUp: function () {
if (this.selectedIndex === -1) { return; }
if (this.selectedIndex === 0) {
this.container.children().get(0).className = '';
this.selectedIndex = -1;
this.el.val(this.currentValue);
return;
}
this.adjustScroll(this.selectedIndex - 1);
},
moveDown: function () {
if (this.selectedIndex === (this.suggestions.length - 1)) { return; }
this.adjustScroll(this.selectedIndex + 1);
},
adjustScroll: function (i) {
var activeItem, offsetTop, upperBound, lowerBound;
activeItem = this.activate(i);
offsetTop = activeItem.offsetTop;
upperBound = this.container.scrollTop();
lowerBound = upperBound + this.options.maxHeight - 25;
if (offsetTop < upperBound) {
this.container.scrollTop(offsetTop);
} else if (offsetTop > lowerBound) {
this.container.scrollTop(offsetTop - this.options.maxHeight + 25);
}
this.el.val(this.getValue(this.suggestions[i]));
},
onSelect: function (i) {
var me, fn, s, d;
me = this;
fn = me.options.onSelect;
s = me.suggestions[i];
d = me.data[i];
me.el.val(me.getValue(s));
if ($.isFunction(fn)) { fn(s, d, me.el); }
},
getValue: function (value) {
var del, currVal, arr, me;
me = this;
del = me.options.delimiter;
if (!del) { return value; }
currVal = me.currentValue;
arr = currVal.split(del);
if (arr.length === 1) { return value; }
return currVal.substr(0, currVal.length - arr[arr.length - 1].length) + value;
}
};
} (jQuery));