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
+2
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 .
@@ -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)
@@ -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()
@@ -0,0 +1 @@
class BeatHaven.Collections.Tracklist extends Backbone.Collection
@@ -0,0 +1,2 @@
class BeatHaven.Models.Album extends Backbone.Model
urlRoot: "/api/albums"
@@ -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(', ')} — #{params.track}")
@@ -0,0 +1,7 @@
class BeatHaven.Models.Track extends Backbone.Model
play: ->
BeatHaven.player.update_title(
artists: @.get("artists")
track: @.get("title")
)
@@ -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)
@@ -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()
)
@@ -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>
@@ -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>
@@ -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>
@@ -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
@@ -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
+126
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
+13 -75
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;
}
}
}
}
}
@@ -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;
}
}
}
}
+28
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
+51
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;
}
}
}
+7
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
+35
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
+74 -2
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
+35 -16
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
+16 -1
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>