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.
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