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