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.
Files changed (36) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE +7 -0
  5. data/README.md +65 -0
  6. data/Rakefile +46 -0
  7. data/bin/soundcloud9000 +9 -0
  8. data/lib/soundcloud9000.rb +22 -0
  9. data/lib/soundcloud9000/application.rb +93 -0
  10. data/lib/soundcloud9000/client.rb +51 -0
  11. data/lib/soundcloud9000/controllers/controller.rb +19 -0
  12. data/lib/soundcloud9000/controllers/player_controller.rb +75 -0
  13. data/lib/soundcloud9000/controllers/track_controller.rb +90 -0
  14. data/lib/soundcloud9000/download_thread.rb +51 -0
  15. data/lib/soundcloud9000/events.rb +17 -0
  16. data/lib/soundcloud9000/models/collection.rb +42 -0
  17. data/lib/soundcloud9000/models/player.rb +138 -0
  18. data/lib/soundcloud9000/models/playlist.rb +22 -0
  19. data/lib/soundcloud9000/models/track.rb +56 -0
  20. data/lib/soundcloud9000/models/track_collection.rb +71 -0
  21. data/lib/soundcloud9000/models/user.rb +26 -0
  22. data/lib/soundcloud9000/time_helper.rb +23 -0
  23. data/lib/soundcloud9000/ui/canvas.rb +21 -0
  24. data/lib/soundcloud9000/ui/color.rb +46 -0
  25. data/lib/soundcloud9000/ui/input.rb +56 -0
  26. data/lib/soundcloud9000/ui/rect.rb +14 -0
  27. data/lib/soundcloud9000/ui/table.rb +132 -0
  28. data/lib/soundcloud9000/ui/view.rb +73 -0
  29. data/lib/soundcloud9000/views/player_view.rb +63 -0
  30. data/lib/soundcloud9000/views/splash.rb +69 -0
  31. data/lib/soundcloud9000/views/tracks_table.rb +14 -0
  32. data/soundcloud9000 +8 -0
  33. data/soundcloud9000.gemspec +27 -0
  34. data/spec/controllers/track_controller_spec.rb +59 -0
  35. data/spec/spec_helper.rb +2 -0
  36. metadata +165 -0
@@ -0,0 +1,51 @@
1
+ require 'net/https'
2
+ require_relative 'events'
3
+
4
+ module Soundcloud9000
5
+ class DownloadThread
6
+ attr_reader :events, :url, :progress, :total, :file
7
+
8
+ def initialize(url, filename)
9
+ @events = Events.new
10
+ @url = URI.parse(url)
11
+ @file = File.open(filename, "w")
12
+ @progress = 0
13
+ start!
14
+ end
15
+
16
+ def log(s)
17
+ Soundcloud9000::Application.logger.debug("DownloadThread #{s}")
18
+ end
19
+
20
+ def start!
21
+ Thread.start do
22
+ begin
23
+ log :start
24
+
25
+ http = Net::HTTP.new(url.host, url.port)
26
+ http.use_ssl = true
27
+
28
+ http.request(Net::HTTP::Get.new(url.request_uri)) do |res|
29
+ log "response: #{res.code}"
30
+ raise res.body if res.code != '200'
31
+
32
+ @total = res.header['Content-Length'].to_i
33
+
34
+ res.read_body do |chunk|
35
+ @progress += chunk.size
36
+ @file << chunk
37
+ @file.close if @progress == @total
38
+ end
39
+ end
40
+ rescue => e
41
+ log e.message
42
+ end
43
+ end
44
+
45
+ sleep 0.1 while @total.nil?
46
+ sleep 0.1
47
+
48
+ self
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,17 @@
1
+ module Soundcloud9000
2
+ class Events
3
+ def initialize
4
+ @handlers = Hash.new { |h, k| h[k] = [] }
5
+ end
6
+
7
+ def on(event, &block)
8
+ @handlers[event] << block
9
+ end
10
+
11
+ def trigger(event, *args)
12
+ @handlers[event].each do |handler|
13
+ handler.call(*args)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,42 @@
1
+ require_relative '../events'
2
+
3
+ module Soundcloud9000
4
+ module Models
5
+ # stores the tracks displayed in the track controller
6
+ class Collection
7
+ include Enumerable
8
+ attr_reader :events, :rows, :page
9
+
10
+ def initialize(client)
11
+ @client = client
12
+ @events = Events.new
13
+ clear
14
+ end
15
+
16
+ def [](*args)
17
+ @rows[*args]
18
+ end
19
+
20
+ def clear
21
+ @page = 0
22
+ @rows = []
23
+ @loaded = false
24
+ end
25
+
26
+ def each(&block)
27
+ @rows.each(&block)
28
+ end
29
+
30
+ def replace(rows)
31
+ clear
32
+ @rows = rows
33
+ events.trigger(:replace)
34
+ end
35
+
36
+ def append(rows)
37
+ @rows += rows
38
+ events.trigger(:append)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,138 @@
1
+ require 'audite'
2
+ require_relative '../download_thread'
3
+
4
+ module Soundcloud9000
5
+ module Models
6
+ # responsible for drawing and updating the player above tracklist
7
+ class Player
8
+ attr_reader :track, :events
9
+
10
+ def initialize
11
+ @track = nil
12
+ @events = Events.new
13
+ @folder = File.expand_path('~/.soundcloud9000')
14
+ @seek_speed = {}
15
+ @seek_time = {}
16
+ create_player
17
+
18
+ Dir.mkdir(@folder) unless File.exist?(@folder)
19
+ end
20
+
21
+ def create_player
22
+ @player = Audite.new
23
+ @player.events.on(:position_change) do
24
+ events.trigger(:progress)
25
+ end
26
+
27
+ @player.events.on(:complete) do
28
+ events.trigger(:complete)
29
+ end
30
+ end
31
+
32
+ def play(track, location)
33
+ log :play, track.id
34
+ @track = track
35
+ load(track, location)
36
+ start
37
+ end
38
+
39
+ def play_progress
40
+ seconds_played / duration
41
+ end
42
+
43
+ def duration
44
+ @track.duration.to_f / 1000
45
+ end
46
+
47
+ def title
48
+ [@track.title, @track.user.username].join(' - ')
49
+ end
50
+
51
+ def length_in_seconds
52
+ mpg = Mpg123.new(@file)
53
+ mpg.length * mpg.tpf / mpg.spf
54
+ end
55
+
56
+ def load(track, location)
57
+ @file = "#{@folder}/#{track.id}.mp3"
58
+
59
+ if !File.exist?(@file) || track.duration / 1000 > length_in_seconds * 0.95
60
+ File.unlink(@file) rescue nil
61
+ @download = DownloadThread.new(location, @file)
62
+ else
63
+ @download = nil
64
+ end
65
+
66
+ @player.load(@file)
67
+ end
68
+
69
+ def log(*args)
70
+ Soundcloud9000::Application.logger.debug 'Player: ' + args.join(' ')
71
+ end
72
+
73
+ def level
74
+ @player.level
75
+ end
76
+
77
+ def seconds_played
78
+ @player.position
79
+ end
80
+
81
+ def download_progress
82
+ @download ? @download.progress / @download.total.to_f : 1
83
+ end
84
+
85
+ def playing?
86
+ @player.active
87
+ end
88
+
89
+ def seek_speed(direction)
90
+ if @seek_time[direction] && Time.now - @seek_time[direction] < 0.5
91
+ @seek_speed[direction] *= 1.05
92
+ else
93
+ @seek_speed[direction] = 1
94
+ end
95
+
96
+ @seek_time[direction] = Time.now
97
+ @seek_speed[direction]
98
+ end
99
+
100
+ # change song position
101
+ def seek_position(position)
102
+ position *= 0.1
103
+ relative_position = position * duration
104
+ if relative_position < seconds_played
105
+ difference = seconds_played - relative_position
106
+ @player.rewind(difference)
107
+ elsif download_progress > (relative_position / duration) && relative_position > seconds_played
108
+ log download_progress
109
+ difference = relative_position - seconds_played
110
+ @player.forward(difference)
111
+ end
112
+ end
113
+
114
+ def rewind
115
+ @player.rewind(seek_speed(:rewind))
116
+ end
117
+
118
+ def forward
119
+ seconds = seek_speed(:forward)
120
+
121
+ seek_percentage = (seconds + seconds_played) / duration
122
+ @player.forward(seconds) if seek_percentage < download_progress
123
+ end
124
+
125
+ def stop
126
+ @player.stop_stream
127
+ end
128
+
129
+ def start
130
+ @player.start_stream
131
+ end
132
+
133
+ def toggle
134
+ @player.toggle
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,22 @@
1
+ module Soundcloud9000
2
+ module Models
3
+ # stores information on a playlist or set from soundcloud
4
+ class Playlist
5
+ def initialize(hash)
6
+ @hash = hash
7
+ end
8
+
9
+ def id
10
+ @hash['id']
11
+ end
12
+
13
+ def title
14
+ @hash['title']
15
+ end
16
+
17
+ def uri
18
+ @hash['uri']
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,56 @@
1
+ require_relative 'user'
2
+
3
+ module Soundcloud9000
4
+ module Models
5
+ # stores information for each track that hits the player
6
+ class Track
7
+ def initialize(hash)
8
+ @hash = hash
9
+ end
10
+
11
+ def id
12
+ @hash['id']
13
+ end
14
+
15
+ def title
16
+ @hash['title']
17
+ end
18
+
19
+ def url
20
+ @hash['permalink_url']
21
+ end
22
+
23
+ def user
24
+ @user ||= User.new(@hash['user'])
25
+ end
26
+
27
+ def username
28
+ user.username
29
+ end
30
+
31
+ def duration
32
+ @hash['duration']
33
+ end
34
+
35
+ def length
36
+ TimeHelper.duration(duration)
37
+ end
38
+
39
+ def plays
40
+ @hash['playback_count']
41
+ end
42
+
43
+ def likes
44
+ @hash['favoritings_count']
45
+ end
46
+
47
+ def comments
48
+ @hash['comments']
49
+ end
50
+
51
+ def stream_url
52
+ @hash['stream_url']
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,71 @@
1
+ require_relative 'collection'
2
+ require_relative 'track'
3
+ require_relative 'playlist'
4
+
5
+ module Soundcloud9000
6
+ module Models
7
+ # This model deals with the different types of tracklists that populate
8
+ # the tracklist section
9
+ class TrackCollection < Collection
10
+ DEFAULT_LIMIT = 50
11
+
12
+ attr_reader :limit
13
+ attr_accessor :collection_to_load, :user, :playlist
14
+
15
+ def initialize(client)
16
+ super
17
+ @limit = DEFAULT_LIMIT
18
+ @collection_to_load = :recent
19
+ end
20
+
21
+ def size
22
+ @rows.size
23
+ end
24
+
25
+ def clear_and_replace
26
+ clear
27
+ load_more
28
+ events.trigger(:replace)
29
+ end
30
+
31
+ def load
32
+ clear
33
+ load_more
34
+ end
35
+
36
+ def load_more
37
+ unless @loaded
38
+ tracks = send(@collection_to_load.to_s + '_tracks')
39
+ @loaded = true if tracks.empty?
40
+ append tracks.map { |hash| Track.new hash }
41
+ @page += 1
42
+ end
43
+ end
44
+
45
+ def favorites_tracks
46
+ return [] if @client.current_user.nil?
47
+ @client.get(@client.current_user.uri + '/favorites', offset: @limit * @page, limit: @limit)
48
+ end
49
+
50
+ def recent_tracks
51
+ @client.get('/tracks', offset: @page * limit, limit: @limit)
52
+ end
53
+
54
+ def user_tracks
55
+ return [] if @client.current_user.nil?
56
+ user_tracks = @client.get(@client.current_user.uri + '/tracks', offset: @limit * @page, limit: @limit)
57
+ if user_tracks.empty?
58
+ UI::Input.error("'#{@client.current_user.username}' has not authored any tracks. Use f to switch to their favorites, or s to switch to their playlists.")
59
+ return []
60
+ else
61
+ return user_tracks
62
+ end
63
+ end
64
+
65
+ def playlist_tracks
66
+ return [] if @playlist.nil?
67
+ @client.get(@playlist.uri + '/tracks', offset: @limit * @page, limit: @limit)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,26 @@
1
+ module Soundcloud9000
2
+ module Models
3
+ # stores information on the current user we are looking at
4
+ class User
5
+ def initialize(hash)
6
+ @hash = hash
7
+ end
8
+
9
+ def id
10
+ @hash['id']
11
+ end
12
+
13
+ def username
14
+ @hash['username']
15
+ end
16
+
17
+ def uri
18
+ @hash['uri']
19
+ end
20
+
21
+ def permalink
22
+ @hash['permalink']
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,23 @@
1
+ require 'time'
2
+
3
+ module Soundcloud9000
4
+ # handles proper time display
5
+ # TODO: make this better looking or find an alternative
6
+ module TimeHelper
7
+ HOUR = 1000 * 60 * 60
8
+ MINUTE = 1000 * 60
9
+ SECONDS = 1000
10
+
11
+ def self.duration(milliseconds)
12
+ parts = [
13
+ milliseconds / 1000 / 60 / 60, # hours
14
+ milliseconds / 1000 / 60 % 60, # minutes
15
+ milliseconds / 1000 % 60, # seconds
16
+ ]
17
+
18
+ parts.shift if parts.first.zero?
19
+
20
+ [parts.first, *parts[1..-1].map { |part| format('%02d', part) }].join('.')
21
+ end
22
+ end
23
+ end