soundcloud9000 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 +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
|