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