soundcloud2000 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|