soundcloud2000 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +5 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE +7 -0
  5. data/README.md +55 -0
  6. data/Rakefile +46 -0
  7. data/bin/soundcloud2000 +5 -0
  8. data/lib/soundcloud2000.rb +18 -0
  9. data/lib/soundcloud2000/application.rb +91 -0
  10. data/lib/soundcloud2000/client.rb +51 -0
  11. data/lib/soundcloud2000/controllers/controller.rb +19 -0
  12. data/lib/soundcloud2000/controllers/player_controller.rb +66 -0
  13. data/lib/soundcloud2000/controllers/track_controller.rb +81 -0
  14. data/lib/soundcloud2000/download_thread.rb +47 -0
  15. data/lib/soundcloud2000/events.rb +17 -0
  16. data/lib/soundcloud2000/models/collection.rb +41 -0
  17. data/lib/soundcloud2000/models/player.rb +141 -0
  18. data/lib/soundcloud2000/models/playlist.rb +23 -0
  19. data/lib/soundcloud2000/models/track.rb +57 -0
  20. data/lib/soundcloud2000/models/track_collection.rb +64 -0
  21. data/lib/soundcloud2000/models/user.rb +26 -0
  22. data/lib/soundcloud2000/time_helper.rb +22 -0
  23. data/lib/soundcloud2000/ui/canvas.rb +22 -0
  24. data/lib/soundcloud2000/ui/color.rb +46 -0
  25. data/lib/soundcloud2000/ui/input.rb +48 -0
  26. data/lib/soundcloud2000/ui/rect.rb +14 -0
  27. data/lib/soundcloud2000/ui/table.rb +131 -0
  28. data/lib/soundcloud2000/ui/view.rb +73 -0
  29. data/lib/soundcloud2000/views/player_view.rb +66 -0
  30. data/lib/soundcloud2000/views/splash.rb +69 -0
  31. data/lib/soundcloud2000/views/tracks_table.rb +13 -0
  32. data/soundcloud2000 +8 -0
  33. data/soundcloud2000.gemspec +25 -0
  34. data/spec/controllers/track_controller_spec.rb +59 -0
  35. data/spec/spec_helper.rb +2 -0
  36. 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,14 @@
1
+ module Soundcloud2000
2
+ module UI
3
+ class Rect
4
+ attr_reader :x, :y, :width, :height
5
+
6
+ def initialize(x, y, width, height)
7
+ @x = x
8
+ @y = y
9
+ @width = width
10
+ @height = height
11
+ end
12
+ end
13
+ end
14
+ 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