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