forked from jxa/ruxtape
-
Notifications
You must be signed in to change notification settings - Fork 0
/
ruxtape.rb
executable file
·436 lines (408 loc) · 15.2 KB
/
ruxtape.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
#!/usr/bin/env ruby
Dir.glob(File.join(File.dirname(__FILE__),"/vendor/*")).each do |lib|
$:.unshift File.join(lib, "/lib")
end
%w(camping camping/session fileutils yaml base64 uri
builder mime/types mp3info openid).each { |lib| require lib}
Camping.goes :Ruxtape
module Ruxtape
include Camping::Session
MP3_PATH = File.join(File.expand_path(File.dirname(__FILE__)), 'public', 'songs')
@@state_secret = "27c9436319ae7c1e760dbd344de08f82b4c7cfcf"
VERSION = "0.1"
end
module Ruxtape::Models
class Config
CONFIG_FILE = File.join(File.expand_path(File.dirname(__FILE__)), 'config', 'config.yml')
class << self
def delete; File.delete(CONFIG_FILE); Mixtape.delete; end
def setup?; return true if File.exist?(CONFIG_FILE) end
def values; YAML.load_file(CONFIG_FILE); end
def setup(openid)
File.open(CONFIG_FILE, "w") { |f| YAML.dump({:openid => openid}, f) }
end
def write(configs)
values = self.values
File.open(CONFIG_FILE, "w") { |f| YAML.dump(values.merge(configs), f) }
end
def writable?; File.exist?(CONFIG_FILE) && File.writable?(CONFIG_FILE); end
def admin?(ident)
if setup?
values[:openid] == ident
else
setup(ident)
true
end
end
end
end
class Mixtape
class << self
def delete
Dir.glob("#{Ruxtape::MP3_PATH}/*.mp3").each { |mp3| File.delete(mp3) }
end
def playlist
songs = []
Dir.glob("#{Ruxtape::MP3_PATH}/*.mp3") { |mp3| songs << Song.new(mp3) }
songs.sort
end
def writable?; Config.writable? && Song.writable?; end
def song_count; Dir.glob("#{Ruxtape::MP3_PATH}/*.mp3").length; end
def length
minutes, seconds = 0,0
self.playlist.each { |song| time = song.time.split(':'); minutes += time[0].to_i; seconds += time[1].to_i }
sec_minutes = (seconds/60).to_i
minutes += sec_minutes; seconds = seconds - (sec_minutes*60)
seconds = "0#{seconds}" if seconds.to_s.size == 1
"#{minutes}:#{seconds}"
end
end
end
class Song
attr_accessor :title, :artist, :length, :filename, :tracknum
attr_reader :path
def initialize(path)
@path = path
self.filename = File.basename(path)
Mp3Info.open(path) do |mp3|
self.title, self.artist, self.length, self.tracknum = mp3.tag.title, mp3.tag.artist, mp3.length, mp3.tag.tracknum
end
end
def self.filename_to_path(filename); File.join(Ruxtape::MP3_PATH, filename); end
def self.writable?; File.writable?(Ruxtape::MP3_PATH); end
def delete; File.delete(self.path); end
def time
minutes = (length/60).to_i; seconds = (((length/60) - minutes) * 60).to_i
"#{minutes}:#{seconds}"
end
def update(attrs)
Mp3Info.open(self.path) do |mp3|
mp3.tag.title = attrs[:title] if attrs[:title]
mp3.tag.artist = attrs[:artist] if attrs[:artist]
mp3.tag.tracknum = attrs[:tracknum].to_i if attrs[:tracknum]
end
end
def url_path; "/songs/#{URI.escape(File.basename(path))}"; end
def <=>(other)
self.tracknum <=> other.tracknum
end
end
end
module Ruxtape::Controllers
class Index < R '/'
def get
if Config.setup? && Mixtape.writable?
@songs = Mixtape.playlist
render(:index)
elsif Mixtape.writable?
render(:setup)
else
@dirs = ['config', 'public/songs']
render(:writable)
end
end
end
class Admin < R '/admin'
def get
return redirect('/setup') unless @state.identity
@songs = Mixtape.playlist
@configs = Config.values
render :admin
end
end
class Login < R '/login'
def get
this_url = URL('/login').to_s
if input.finish.to_s != '1'
begin
request_state = { }
oid_request = OpenID::Consumer.new(request_state, nil).begin(input.openid_identifier)
oid_request.return_to_args['finish'] = '1'
@state.openid_request = Marshal.dump(request_state)
redirect(oid_request.redirect_url(URL('/').to_s, this_url))
rescue OpenID::DiscoveryFailure
return 'Couldn\'t find an OpenID at that address, are you sure it is one?'
end
else
request_state = Marshal.restore(@state.openid_request)
response = OpenID::Consumer.new(request_state, nil).complete(input, this_url)
@state.delete('openid_request')
case response.status
when OpenID::Consumer::SUCCESS
identity = response.identity_url.to_s.sub(/^http:\/\//, '').sub(/\/$/,'')
return redirect(R(Setup)) unless Config.admin?(identity)
@state.identity = identity
return redirect(R(Admin))
when OpenID::Consumer::FAILURE
'The OpenID thing doesn\'t think you really are that person, they said: ' + response.message
else
raise
end
end
end
end
class Logout < R '/logout'
def get; @state.identity = nil; redirect R(Index); end
end
class Restart < R '/admin/restart'
def post
return unless signed?
return redirect('/setup') unless @state.identity
Config.delete; redirect R(Index)
end
end
class UpdateSong < R '/admin/update_song'
def post
return redirect('/setup') unless @state.identity
path = Song.filename_to_path(input.song_filename)
@song = Song.new(path)
@song.update(:artist => input.song_artist, :title => input.song_title)
redirect R(Admin)
end
end
class DeleteSong < R '/admin/delete_song'
def post
return redirect('/setup') unless @state.identity
path = Song.filename_to_path(input.song_filename)
@song = Song.new(path)
@song.delete
redirect R(Admin)
end
end
class UpdateConfig < R '/admin/update_config'
def post
configs = { :openid => input.config_openid, :google => input.config_google }
Config.write(configs)
redirect R(Admin)
end
end
class Setup < R '/setup'
def get; Config.setup? ? render(:setup) : redirect(R(Index)); end
def post
unless Config.setup?
Config.setup(:openid => input.openid_identifier, :google => "")
redirect R(Login, :signed => sign)
else
redirect R(Index)
end
end
end
class Upload < R '/admin/upload'
include FileUtils::Verbose
def post
return unless signed?
return redirect('/setup') unless @state.identity
@path = File.join(Ruxtape::MP3_PATH, input.file[:filename])
return redirect(R(Admin)) if @path == Ruxtape::MP3_PATH
cp(input.file[:tempfile].path, @path)
Song.new(@path).update(:tracknum => Mixtape.song_count)
redirect R(Admin)
end
end
class Reorder < R '/admin/reorder'
def post
return "invalid request" unless signed?
if input.songs
p input.songs
songs = input.songs.map { |filename| Song.new(File.join(Ruxtape::MP3_PATH, filename)) }
songs.each_with_index { |song, i| song.update :tracknum => i+1 }
end
"ok"
end
end
class Static < R '/(assets|songs)/(.+)'
MIME_TYPES = {'.css' => 'text/css', '.js' => 'text/javascript', '.swf' => "application/x-shockwave-flash", '.mp3' => 'audio/mpeg'}
PATH = File.join(File.expand_path(File.dirname(__FILE__)), 'public')
def get(type, path)
@headers['Content-Type'] = MIME_TYPES[path[/\.\w+$/, 0]] || "text/plain"
unless path.include? ".." # prevent directory traversal attacks
file = "#{PATH}/#{type}/#{path}"
@headers['X-Sendfile'] = "#{PATH}/#{type}/#{URI.unescape(path)}"
else
@status = "403"
"403 - Invalid path"
end
end
end
end
module Ruxtape::Helpers
# used to sign url's to stopXSS attacks
def sign; @state.request_signature ||= rand(39_000).to_s(16); end
def signed?; input.signed == @state.request_signature; end
end
module Ruxtape::Views
def layout
xhtml_transitional do
head do
title "Ruxtape => Punks jump up to get beat down."
link(:rel => 'shortcut icon', :href => '/assets/images/favicon.ico')
link(:rel => 'stylesheet', :type => 'text/css',
:href => '/assets/styles.css', :media => 'screen' )
link(:rel => 'stylesheet', :type => 'text/css',
:href => '/assets/page-player.css', :media => 'screen' )
link(:rel => 'stylesheet', :type => 'text/css',
:href => '/assets/inline-player.css', :media => 'screen' )
meta(:content => 'noindex, nofollow', :name => "robots")
%w(jquery.js ui.core.js ui.sortable.js ruxtape.js).each do |lib|
script(:type => 'text/javascript', :src => "/assets/#{lib}")
script(:type => 'text/javascript', :src => '/assets/soundmanager/soundmanager2-nodebug-jsmin.js')
script :type => 'text/javascript' do "soundManager.url = '../../assets/soundmanager';" end
script :type => 'text/javascript' do
"var PP_CONFIG = {flashVersion: 9,usePeakData: true,useWaveformData: false,useEQData: false,useFavIcon: false}"
end
end
end
body do
div.wrapper! do
div.header! do
if @state.identity
div.manage_button do a "Manage", :href => "/admin" end
div.manage_button do a "Listen", :href => "/" end
end
div.title! { "Ruxtape, sucka"}
div.subtitle! {"#{Ruxtape::Models::Mixtape.song_count} songs / (#{Ruxtape::Models::Mixtape.length})"}
end
div.pinstripe do "" end
self << yield
div.footer! do
a "Ruxtape #{Ruxtape::VERSION}", :href => "http://github.com/ch0wda/ruxtape"
text " » "
@state.identity ? a("Logout", :href => R(Logout)) : a("Login", :href => "/setup")
end
end
_google_analytics unless (Ruxtape::Models::Config.values[:google] == (nil || "") )
end
end
end
def index
# The order of the following js calls is apparently quite critical to proper behaviour.
script :type => 'text/javascript' do
"var PP_CONFIG = {flashVersion: 9,usePeakData: true,useWaveformData: false,useEQData: false,useFavIcon: false}"
end
script(:type => 'text/javascript', :src => '/assets/soundmanager/page-player.js')
script :type => 'text/javascript' do "soundManager.url = '../../assets/soundmanager';" end
div.warning! {"You do not have javascript enabled, this site will not work without it."}
ul :class => 'playlist' do
@songs.each { |song| li {a("#{song.artist} - #{song.title}", :href=> song.url_path) }}
end
_tape_controls
end
def setup
div.content! do
h1 "Get Mixin'"
p "Type in your OpenID address below to get started."
form({ :method => 'get', :action => R(Login, :signed => sign)}) do
input :type => "text", :name => "openid_identifier"
button :name => "submit", :class => 'darkBtn' do span 'Login' end
end
end
end
def writable
div.content! do
h1 "System Problem"
p "Make sure these folders are writable by the process Ruxtape runs as:"
ul do
@dirs.each do |dir|
li dir
end
end
end
end
def admin
script(:type => 'text/javascript', :src => '/assets/soundmanager/inline-player.js')
div.content! do
p.login { text "You are authenticated as #{@state.identity}." }
h1 "Switch Up Your Tape"
p 'You can upload another song, rearrange your mix, or blow it all away.'
div.admin_area do
div.admin_left do
h2 "Upload a New Jam"
div.graybox do
form({ :method => 'post', :enctype => "multipart/form-data",
:action => R(Upload, :signed => sign)}) do
p.center do input :type => "file", :name => "file", :value => "Browse" end
p.center do button :name => "submit", :class => 'darkBtn' do span 'Upload' end end
end
end
h2 "Configuration"
div.graybox do
form({ :method => 'post', :action => R(UpdateConfig, :signed => sign)}) do
@configs.each do |key, value|
p.center do label "#{key.to_s.capitalize}", :for => "config_#{key}"
input :type => "text", :name => "config_#{key}", :value => value end
end
p.center do button :name => "submit", :class => 'darkBtn' do span 'Save' end end
end
end
div.warning do
h2 "Reset"
div.graybox do
p "This will reset your ruxtape by deleting all your songs and the admin account taking you back to the original setup."
form({ :method => 'post', :action => R(Restart, :signed => sign)}) do
p.center do button :name => "submit", :class => 'redBtn' do span 'Reset' end end
end
end
end
end
div.admin_list do
h2 "Edit Playlist"
ul.sorter.sorter! do
@songs.each do |song|
li.sortable(:id => "songs_#{song.filename}") { _song_admin(song) }
end
end
span.signature! sign
end
end
end
end
def _song_admin(song)
div.song do
div.info do
h3 do a("#{song.artist} - #{song.title}", :href=> song.url_path, :class => 'sm2_link inline') end
div.edit_song_controls do
span.edit_song_button {"Edit Song"}
form.delete({ :method => 'post', :action => R(DeleteSong, :signed => sign)}) do
input :type => "hidden", :name => "song_filename", :value => song.filename
button :name => "submit", :class => 'redBtn' do span 'Delete' end
end
end
end
div.edit_song do
div.edit_song_form do
h6 "file - (#{song.filename})"
form({ :method => 'post', :action => R(UpdateSong, :signed => sign)}) do
label 'Artist ', :for => 'song_artist'
input :type => "text", :name => "song_artist", :value => song.artist
label 'Song ', :for => 'song_title'
input :type => "text", :name => "song_title", :value => song.title
input :type => "hidden", :name => "song_filename", :value => song.filename
input :type => "submit", :value => "Save"
end
end
end
end
end
def _tape_controls
div :id => 'control-template' do
div.controls { div.statusbar { div.loading ''; div.position ''} }
div.timing do
div :id => "sm2_timing", :class => 'timing-data' do
span.sm2_position {"%s1"}; span.sm2_total {" / %s2"}
end
end
div.peak { div(:class => 'peak-box') { span :class => 'l'; span :class => 'r' }}
end
div(:id =>'spectrum-container', :class => 'spectrum-container') do
div(:class => 'spectrum-box') { div.spectrum {""} }
end
end
def _google_analytics
script :type => 'text/javascript' do
"var gaJsHost = ((\"https:\" == document.location.protocol) ? \"https://ssl.\" : \"http://www.\"); document.write(unescape(\"%3Cscript src='\" + gaJsHost + \"google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E\"));"
end
script :type => 'text/javascript' do
"var pageTracker = _gat._getTracker(\"#{Ruxtape::Models::Config.values[:google]}\"); pageTracker._trackPageview();"
end
end
end