soundcloud2000 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.gitignore +5 -0
- data/Gemfile +4 -0
- data/LICENSE +7 -0
- data/README.md +55 -0
- data/Rakefile +46 -0
- data/bin/soundcloud2000 +5 -0
- data/lib/soundcloud2000.rb +18 -0
- data/lib/soundcloud2000/application.rb +91 -0
- data/lib/soundcloud2000/client.rb +51 -0
- data/lib/soundcloud2000/controllers/controller.rb +19 -0
- data/lib/soundcloud2000/controllers/player_controller.rb +66 -0
- data/lib/soundcloud2000/controllers/track_controller.rb +81 -0
- data/lib/soundcloud2000/download_thread.rb +47 -0
- data/lib/soundcloud2000/events.rb +17 -0
- data/lib/soundcloud2000/models/collection.rb +41 -0
- data/lib/soundcloud2000/models/player.rb +141 -0
- data/lib/soundcloud2000/models/playlist.rb +23 -0
- data/lib/soundcloud2000/models/track.rb +57 -0
- data/lib/soundcloud2000/models/track_collection.rb +64 -0
- data/lib/soundcloud2000/models/user.rb +26 -0
- data/lib/soundcloud2000/time_helper.rb +22 -0
- data/lib/soundcloud2000/ui/canvas.rb +22 -0
- data/lib/soundcloud2000/ui/color.rb +46 -0
- data/lib/soundcloud2000/ui/input.rb +48 -0
- data/lib/soundcloud2000/ui/rect.rb +14 -0
- data/lib/soundcloud2000/ui/table.rb +131 -0
- data/lib/soundcloud2000/ui/view.rb +73 -0
- data/lib/soundcloud2000/views/player_view.rb +66 -0
- data/lib/soundcloud2000/views/splash.rb +69 -0
- data/lib/soundcloud2000/views/tracks_table.rb +13 -0
- data/soundcloud2000 +8 -0
- data/soundcloud2000.gemspec +25 -0
- data/spec/controllers/track_controller_spec.rb +59 -0
- data/spec/spec_helper.rb +2 -0
- metadata +149 -0
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'curses'
|
2
|
+
|
3
|
+
require_relative 'color'
|
4
|
+
|
5
|
+
module Soundcloud2000
|
6
|
+
module UI
|
7
|
+
class Canvas
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
Curses.noecho # do not show typed keys
|
11
|
+
Curses.stdscr.keypad(true) # enable arrow keys
|
12
|
+
Curses.init_screen
|
13
|
+
Color.init
|
14
|
+
end
|
15
|
+
|
16
|
+
def close
|
17
|
+
Curses.close_screen
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'curses'
|
2
|
+
|
3
|
+
module Soundcloud2000
|
4
|
+
module UI
|
5
|
+
class Color
|
6
|
+
PAIRS = {
|
7
|
+
:white => 0,
|
8
|
+
:red => 1,
|
9
|
+
:blue => 2,
|
10
|
+
:green => 3,
|
11
|
+
:cyan => 4,
|
12
|
+
}
|
13
|
+
|
14
|
+
DEFINITION = {
|
15
|
+
PAIRS[:white] => [ Curses::COLOR_WHITE, Curses::COLOR_BLACK ],
|
16
|
+
PAIRS[:red] => [ Curses::COLOR_RED, Curses::COLOR_WHITE ],
|
17
|
+
PAIRS[:blue] => [ Curses::COLOR_BLUE, Curses::COLOR_WHITE ],
|
18
|
+
PAIRS[:green] => [ Curses::COLOR_GREEN, Curses::COLOR_BLACK ],
|
19
|
+
PAIRS[:cyan] => [ Curses::COLOR_BLACK, Curses::COLOR_CYAN ],
|
20
|
+
}
|
21
|
+
|
22
|
+
COLORS = {
|
23
|
+
:white => Curses.color_pair(PAIRS[:white]),
|
24
|
+
:black => Curses.color_pair(PAIRS[:white])|Curses::A_REVERSE,
|
25
|
+
:red => Curses.color_pair(PAIRS[:red]),
|
26
|
+
:blue => Curses.color_pair(PAIRS[:blue]),
|
27
|
+
:green => Curses.color_pair(PAIRS[:green]),
|
28
|
+
:green_reverse => Curses.color_pair(PAIRS[:green])|Curses::A_REVERSE,
|
29
|
+
:cyan => Curses.color_pair(PAIRS[:cyan]),
|
30
|
+
}
|
31
|
+
|
32
|
+
def self.init
|
33
|
+
Curses.start_color
|
34
|
+
|
35
|
+
DEFINITION.each do |definition, (color, background)|
|
36
|
+
Curses.init_pair(definition, color, background)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.get(name)
|
41
|
+
COLORS[name]
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'curses'
|
2
|
+
|
3
|
+
module Soundcloud2000
|
4
|
+
module UI
|
5
|
+
class Input
|
6
|
+
MAPPING = {
|
7
|
+
Curses::KEY_LEFT => :left,
|
8
|
+
Curses::KEY_RIGHT => :right,
|
9
|
+
Curses::KEY_DOWN => :down,
|
10
|
+
Curses::KEY_UP => :up,
|
11
|
+
Curses::KEY_CTRL_J => :enter,
|
12
|
+
Curses::KEY_ENTER => :enter,
|
13
|
+
' ' => :space,
|
14
|
+
'j' => :j,
|
15
|
+
'k' => :k,
|
16
|
+
's' => :s,
|
17
|
+
'u' => :u,
|
18
|
+
'1' => :one,
|
19
|
+
'2' => :two,
|
20
|
+
'3' => :three,
|
21
|
+
'4' => :four,
|
22
|
+
'5' => :five,
|
23
|
+
'6' => :six,
|
24
|
+
'7' => :seven,
|
25
|
+
'8' => :eight,
|
26
|
+
'9' => :nine,
|
27
|
+
'f' => :f
|
28
|
+
}
|
29
|
+
|
30
|
+
def self.get(delay = 0)
|
31
|
+
Curses.timeout = delay
|
32
|
+
MAPPING[Curses.getch]
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.getstr(prompt)
|
36
|
+
Curses.setpos(Curses.lines - 1, 0)
|
37
|
+
Curses.addstr(prompt)
|
38
|
+
Curses.echo
|
39
|
+
result = Curses.getstr
|
40
|
+
Curses.noecho
|
41
|
+
Curses.setpos(Curses.lines - 1, 0)
|
42
|
+
Curses.addstr(''.ljust(Curses.cols))
|
43
|
+
result
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
require_relative 'view'
|
2
|
+
|
3
|
+
module Soundcloud2000
|
4
|
+
module UI
|
5
|
+
class Table < View
|
6
|
+
SEPARATOR = ' | '
|
7
|
+
|
8
|
+
attr_reader :current, :collection
|
9
|
+
attr_accessor :header, :keys
|
10
|
+
|
11
|
+
def initialize(*args)
|
12
|
+
super
|
13
|
+
|
14
|
+
@sizes = []
|
15
|
+
@rows = []
|
16
|
+
@current, @top = 0, 0
|
17
|
+
@selected = nil
|
18
|
+
|
19
|
+
reset
|
20
|
+
end
|
21
|
+
|
22
|
+
def bind_to(collection)
|
23
|
+
raise ArgumentError if @collection
|
24
|
+
|
25
|
+
@collection = collection
|
26
|
+
@collection.events.on(:append) { render }
|
27
|
+
@collection.events.on(:replace) { clear; render }
|
28
|
+
end
|
29
|
+
|
30
|
+
def length
|
31
|
+
@collection.size
|
32
|
+
end
|
33
|
+
|
34
|
+
def body_height
|
35
|
+
rect.height - @header.size
|
36
|
+
end
|
37
|
+
|
38
|
+
def bottom?
|
39
|
+
current + 1 >= length
|
40
|
+
end
|
41
|
+
|
42
|
+
def up
|
43
|
+
if @current > 0
|
44
|
+
@current -= 1
|
45
|
+
@top -= 1 if @current < @top
|
46
|
+
render
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def down
|
51
|
+
if (@current + 1) < length
|
52
|
+
@current += 1
|
53
|
+
@top += 1 if @current > body_height
|
54
|
+
render
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def select
|
59
|
+
@selected = @current
|
60
|
+
render
|
61
|
+
end
|
62
|
+
|
63
|
+
def deselect
|
64
|
+
@selected = nil
|
65
|
+
render
|
66
|
+
end
|
67
|
+
|
68
|
+
protected
|
69
|
+
|
70
|
+
def rows(start = 0, size = collection.size)
|
71
|
+
collection[start, size].map do |record|
|
72
|
+
keys.map {|key| record.send(key).to_s }
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def rest_width(elements)
|
77
|
+
rect.width - elements.size * SEPARATOR.size -
|
78
|
+
elements.inject(0) { |sum, size| sum += size }
|
79
|
+
end
|
80
|
+
|
81
|
+
def perform_layout
|
82
|
+
@sizes = []
|
83
|
+
(rows + [header]).each do |row|
|
84
|
+
row.each_with_index do |value, index|
|
85
|
+
current, max = value.to_s.length, @sizes[index] || 0
|
86
|
+
@sizes[index] = current if max < current
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
@sizes[-1] = rest_width(@sizes[0...-1])
|
91
|
+
end
|
92
|
+
|
93
|
+
def draw
|
94
|
+
draw_header
|
95
|
+
draw_body
|
96
|
+
end
|
97
|
+
|
98
|
+
def draw_header
|
99
|
+
with_color(:green_reverse) do
|
100
|
+
draw_values(header)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def color_for(index)
|
105
|
+
if @top + index == @current
|
106
|
+
:cyan
|
107
|
+
elsif @top + index == @selected
|
108
|
+
:black
|
109
|
+
else
|
110
|
+
:white
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def draw_body
|
115
|
+
rows(@top, body_height + 1).each_with_index do |row, index|
|
116
|
+
with_color(color_for(index)) do
|
117
|
+
draw_values(row)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def draw_values(values)
|
123
|
+
i = -1
|
124
|
+
content = values.map { |value| value.ljust(@sizes[i += 1]) }.join(SEPARATOR)
|
125
|
+
|
126
|
+
line content
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'curses'
|
2
|
+
|
3
|
+
require_relative 'color'
|
4
|
+
|
5
|
+
module Soundcloud2000
|
6
|
+
module UI
|
7
|
+
class View
|
8
|
+
ROW_SEPARATOR = ?|
|
9
|
+
LINE_SEPARATOR = ?-
|
10
|
+
INTERSECTION = ?+
|
11
|
+
|
12
|
+
attr_reader :rect
|
13
|
+
|
14
|
+
def initialize(rect)
|
15
|
+
@rect = rect
|
16
|
+
@window = Curses::Window.new(rect.height, rect.width, rect.y, rect.x)
|
17
|
+
@line = 0
|
18
|
+
@padding = 0
|
19
|
+
end
|
20
|
+
|
21
|
+
def padding(value = nil)
|
22
|
+
value.nil? ? @padding : @padding = value
|
23
|
+
end
|
24
|
+
|
25
|
+
def render
|
26
|
+
perform_layout
|
27
|
+
reset
|
28
|
+
draw
|
29
|
+
refresh
|
30
|
+
end
|
31
|
+
|
32
|
+
def body_width
|
33
|
+
rect.width - 2 * padding
|
34
|
+
end
|
35
|
+
|
36
|
+
def with_color(name, &block)
|
37
|
+
@window.attron(Color.get(name), &block)
|
38
|
+
end
|
39
|
+
|
40
|
+
def clear
|
41
|
+
@window.clear
|
42
|
+
end
|
43
|
+
|
44
|
+
protected
|
45
|
+
|
46
|
+
def lines_left
|
47
|
+
rect.height - @line - 1
|
48
|
+
end
|
49
|
+
|
50
|
+
def line(content)
|
51
|
+
@window.setpos(@line, padding)
|
52
|
+
@window.addstr(content.ljust(body_width).slice(0, body_width))
|
53
|
+
@line += 1
|
54
|
+
end
|
55
|
+
|
56
|
+
def reset
|
57
|
+
@line = 0
|
58
|
+
end
|
59
|
+
|
60
|
+
def refresh
|
61
|
+
@window.refresh
|
62
|
+
end
|
63
|
+
|
64
|
+
def perform_layout
|
65
|
+
end
|
66
|
+
|
67
|
+
def draw
|
68
|
+
raise NotImplementedError
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require_relative '../time_helper'
|
2
|
+
require_relative '../ui/view'
|
3
|
+
|
4
|
+
module Soundcloud2000
|
5
|
+
module Views
|
6
|
+
class PlayerView < UI::View
|
7
|
+
|
8
|
+
def initialize(*attrs)
|
9
|
+
super
|
10
|
+
|
11
|
+
@spectrum = true
|
12
|
+
padding 2
|
13
|
+
end
|
14
|
+
|
15
|
+
def player(instance)
|
16
|
+
@player = instance
|
17
|
+
end
|
18
|
+
|
19
|
+
def toggle_spectrum
|
20
|
+
@spectrum = !@spectrum
|
21
|
+
end
|
22
|
+
|
23
|
+
protected
|
24
|
+
|
25
|
+
def draw
|
26
|
+
line progress + download_progress
|
27
|
+
with_color(:green) do
|
28
|
+
line (duration + ' - ' + status).ljust(16) + @player.title
|
29
|
+
end
|
30
|
+
line track_info
|
31
|
+
line '>' * (@player.level.to_f * body_width).ceil
|
32
|
+
end
|
33
|
+
|
34
|
+
def status
|
35
|
+
@player.playing? ? 'playing' : 'paused'
|
36
|
+
end
|
37
|
+
|
38
|
+
def progress
|
39
|
+
'#' * (@player.play_progress * body_width).ceil
|
40
|
+
end
|
41
|
+
|
42
|
+
def download_progress
|
43
|
+
progress = @player.download_progress - @player.play_progress
|
44
|
+
|
45
|
+
if progress > 0
|
46
|
+
'.' * (progress * body_width).ceil
|
47
|
+
else
|
48
|
+
''
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def track
|
53
|
+
@player.track
|
54
|
+
end
|
55
|
+
|
56
|
+
def track_info
|
57
|
+
"#{track.plays} Plays | #{track.likes} Likes | #{track.comments} Comments | #{track.url}"
|
58
|
+
end
|
59
|
+
|
60
|
+
def duration
|
61
|
+
TimeHelper.duration(@player.seconds_played.to_i * 1000)
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require_relative '../ui/view'
|
2
|
+
|
3
|
+
module Soundcloud2000
|
4
|
+
module Views
|
5
|
+
class Splash < UI::View
|
6
|
+
CONTENT = %q{
|
7
|
+
ohmmNNmmdyoo.
|
8
|
+
dhMMMMMMMMMMMMMMMms-
|
9
|
+
:m .MMMMMMMMMMMMMMMMMMMMd-
|
10
|
+
.o`d: o++./dMy .MMMMMMMMMMMMMMMMMMMMMMd-
|
11
|
+
/y/M.Mo MmdhMoMy -MMMMMMMMMMMMMMMMMMMMMMMN-
|
12
|
+
. +N+M-Ms MNddMoMh /:MMMMMMMMMMMMMMMMMMMMMMMMN-
|
13
|
+
hy yMoM:My MMddMsMd :oMMMMMMMMMMMMMMMMMMMMMMMMMMd
|
14
|
+
_ dh hMsM/Mh .MMdmMyMd /oMMMMMMMMMMMMMMMMMMMMMMMMMMMMMdhoo
|
15
|
+
. m N h:Nm dMyM+Md DMMdNMyMm /MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMms.
|
16
|
+
.m M..M NoMN mMhMsMm dMMdMMhMN +MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMN/
|
17
|
+
d/MM M/MM /oMhMM /MMdMyMN MMMdMMhMN /MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM-
|
18
|
+
:MsMM MsMM +MMNMM +MMmMhMM MMMdMMdMM +MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMy
|
19
|
+
/MyMM MyMM +dMNMM dMMmMyMN dMMdMMdMM +oMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMs
|
20
|
+
N+MM M+MM \+MdMM \NMhMoMm +MMdNMyMm \/MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMm.
|
21
|
+
\+-N M-.M MsNm dMsM/Mh \MMdmMsMd :MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMh.
|
22
|
+
+ N M m+dy +N+M-Ms MmdhMoMy .MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNy:
|
23
|
+
`:- -/.+ +. +: -+.o- +/+oooooooooooooooooooooooooooo+/-
|
24
|
+
|
25
|
+
|
26
|
+
_ _ _ ___ ___ ___ ___
|
27
|
+
| | | | | |__ \ / _ \ / _ \ / _ \
|
28
|
+
___ ___ _ _ _ __ __| | ___| | ___ _ _ __| | ) | | | | | | | | | |
|
29
|
+
/ __|/ _ \| | | | '_ \ / _` |/ __| |/ _ \| | | |/ _` | / /| | | | | | | | | |
|
30
|
+
\__ \ (_) | |_| | | | | (_| | (__| | (_) | |_| | (_| |/ /_| |_| | |_| | |_| |
|
31
|
+
|___/\___/ \__,_|_| |_|\__,_|\___|_|\___/ \__,_|\__,_|____|\___/ \___/ \___/
|
32
|
+
|
33
|
+
Matthias Georgi and Tobias Schmidt
|
34
|
+
Music Hack Day Stockholm 2013
|
35
|
+
}
|
36
|
+
|
37
|
+
protected
|
38
|
+
|
39
|
+
def left
|
40
|
+
(rect.width - lines.map(&:length).max) / 2
|
41
|
+
end
|
42
|
+
|
43
|
+
def top
|
44
|
+
(rect.height - lines.size) / 2
|
45
|
+
end
|
46
|
+
|
47
|
+
def lines
|
48
|
+
CONTENT.split("\n")
|
49
|
+
end
|
50
|
+
|
51
|
+
def draw
|
52
|
+
0.upto(top) { line '' }
|
53
|
+
lines.each do |row|
|
54
|
+
with_color(:green) do
|
55
|
+
line ' ' * left + row
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def refresh
|
61
|
+
super
|
62
|
+
|
63
|
+
# show until any keypress
|
64
|
+
@window.getch
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|