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.
- checksums.yaml +15 -0
- data/.gitignore +5 -0
- data/Gemfile +4 -0
- data/LICENSE +7 -0
- data/README.md +55 -0
- data/Rakefile +46 -0
- data/bin/soundcloud2000 +5 -0
- data/lib/soundcloud2000.rb +18 -0
- data/lib/soundcloud2000/application.rb +91 -0
- data/lib/soundcloud2000/client.rb +51 -0
- data/lib/soundcloud2000/controllers/controller.rb +19 -0
- data/lib/soundcloud2000/controllers/player_controller.rb +66 -0
- data/lib/soundcloud2000/controllers/track_controller.rb +81 -0
- data/lib/soundcloud2000/download_thread.rb +47 -0
- data/lib/soundcloud2000/events.rb +17 -0
- data/lib/soundcloud2000/models/collection.rb +41 -0
- data/lib/soundcloud2000/models/player.rb +141 -0
- data/lib/soundcloud2000/models/playlist.rb +23 -0
- data/lib/soundcloud2000/models/track.rb +57 -0
- data/lib/soundcloud2000/models/track_collection.rb +64 -0
- data/lib/soundcloud2000/models/user.rb +26 -0
- data/lib/soundcloud2000/time_helper.rb +22 -0
- data/lib/soundcloud2000/ui/canvas.rb +22 -0
- data/lib/soundcloud2000/ui/color.rb +46 -0
- data/lib/soundcloud2000/ui/input.rb +48 -0
- data/lib/soundcloud2000/ui/rect.rb +14 -0
- data/lib/soundcloud2000/ui/table.rb +131 -0
- data/lib/soundcloud2000/ui/view.rb +73 -0
- data/lib/soundcloud2000/views/player_view.rb +66 -0
- data/lib/soundcloud2000/views/splash.rb +69 -0
- data/lib/soundcloud2000/views/tracks_table.rb +13 -0
- data/soundcloud2000 +8 -0
- data/soundcloud2000.gemspec +25 -0
- data/spec/controllers/track_controller_spec.rb +59 -0
- data/spec/spec_helper.rb +2 -0
- metadata +149 -0
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require_relative 'events'
|
3
|
+
|
4
|
+
module Soundcloud2000
|
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
|
+
Soundcloud2000::Application.logger.debug("DownloadThread #{s}")
|
18
|
+
end
|
19
|
+
|
20
|
+
def start!
|
21
|
+
Thread.start do
|
22
|
+
begin
|
23
|
+
log :start
|
24
|
+
Net::HTTP.get_response(url) do |res|
|
25
|
+
log "response: #{res.code}"
|
26
|
+
raise res.body if res.code != '200'
|
27
|
+
|
28
|
+
@total = res.header['Content-Length'].to_i
|
29
|
+
|
30
|
+
res.read_body do |chunk|
|
31
|
+
@progress += chunk.size
|
32
|
+
@file << chunk
|
33
|
+
@file.close if @progress == @total
|
34
|
+
end
|
35
|
+
end
|
36
|
+
rescue => e
|
37
|
+
log e.message
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
sleep 0.1 while @total.nil?
|
42
|
+
sleep 0.1
|
43
|
+
|
44
|
+
self
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Soundcloud2000
|
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,41 @@
|
|
1
|
+
require_relative '../events'
|
2
|
+
|
3
|
+
module Soundcloud2000
|
4
|
+
module Models
|
5
|
+
class Collection
|
6
|
+
include Enumerable
|
7
|
+
attr_reader :events, :rows, :page
|
8
|
+
|
9
|
+
def initialize(client)
|
10
|
+
@client = client
|
11
|
+
@events = Events.new
|
12
|
+
clear
|
13
|
+
end
|
14
|
+
|
15
|
+
def [](*args)
|
16
|
+
@rows[*args]
|
17
|
+
end
|
18
|
+
|
19
|
+
def clear
|
20
|
+
@page = 0
|
21
|
+
@rows = []
|
22
|
+
@loaded = false
|
23
|
+
end
|
24
|
+
|
25
|
+
def each(&block)
|
26
|
+
@rows.each(&block)
|
27
|
+
end
|
28
|
+
|
29
|
+
def replace(rows)
|
30
|
+
clear
|
31
|
+
@rows = rows
|
32
|
+
events.trigger(:replace)
|
33
|
+
end
|
34
|
+
|
35
|
+
def append(rows)
|
36
|
+
@rows += rows
|
37
|
+
events.trigger(:append)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
require 'audite'
|
2
|
+
require_relative '../download_thread'
|
3
|
+
|
4
|
+
module Soundcloud2000
|
5
|
+
module Models
|
6
|
+
class Player
|
7
|
+
attr_reader :track, :events
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@track = nil
|
11
|
+
@events = Events.new
|
12
|
+
@folder = File.expand_path("~/.soundcloud2000")
|
13
|
+
@seek_speed = {}
|
14
|
+
@seek_time = {}
|
15
|
+
create_player
|
16
|
+
|
17
|
+
Dir.mkdir(@folder) unless File.exist?(@folder)
|
18
|
+
end
|
19
|
+
|
20
|
+
def create_player
|
21
|
+
@player = Audite.new
|
22
|
+
@player.events.on(:position_change) do |position|
|
23
|
+
events.trigger(:progress)
|
24
|
+
end
|
25
|
+
|
26
|
+
@player.events.on(:complete) do
|
27
|
+
events.trigger(:complete)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def play(track, location)
|
32
|
+
log :play, track.id
|
33
|
+
@track = track
|
34
|
+
load(track, location)
|
35
|
+
start
|
36
|
+
end
|
37
|
+
|
38
|
+
def play_progress
|
39
|
+
seconds_played / duration
|
40
|
+
end
|
41
|
+
|
42
|
+
def duration
|
43
|
+
@track.duration.to_f / 1000
|
44
|
+
end
|
45
|
+
|
46
|
+
def title
|
47
|
+
[@track.title, @track.user.username].join(' - ')
|
48
|
+
end
|
49
|
+
|
50
|
+
def length_in_seconds
|
51
|
+
mpg = Mpg123.new(@file)
|
52
|
+
mpg.length * mpg.tpf / mpg.spf
|
53
|
+
end
|
54
|
+
|
55
|
+
def load(track, location, &block)
|
56
|
+
@file = "#{@folder}/#{track.id}.mp3"
|
57
|
+
|
58
|
+
if !File.exist?(@file) || track.duration / 1000 < length_in_seconds * 0.95
|
59
|
+
File.unlink(@file) rescue nil
|
60
|
+
@download = DownloadThread.new(location, @file)
|
61
|
+
else
|
62
|
+
@download = nil
|
63
|
+
end
|
64
|
+
|
65
|
+
@player.load(@file)
|
66
|
+
end
|
67
|
+
|
68
|
+
def log(*args)
|
69
|
+
Soundcloud2000::Application.logger.debug 'Player: ' + args.join(" ")
|
70
|
+
end
|
71
|
+
|
72
|
+
def level
|
73
|
+
@player.level
|
74
|
+
end
|
75
|
+
|
76
|
+
def seconds_played
|
77
|
+
@player.position
|
78
|
+
end
|
79
|
+
|
80
|
+
def download_progress
|
81
|
+
if @download
|
82
|
+
@download.progress / @download.total.to_f
|
83
|
+
else
|
84
|
+
1
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def playing?
|
89
|
+
@player.active
|
90
|
+
end
|
91
|
+
|
92
|
+
def seek_speed(direction)
|
93
|
+
if @seek_time[direction] && Time.now - @seek_time[direction] < 0.5
|
94
|
+
@seek_speed[direction] *= 1.05
|
95
|
+
else
|
96
|
+
@seek_speed[direction] = 1
|
97
|
+
end
|
98
|
+
|
99
|
+
@seek_time[direction] = Time.now
|
100
|
+
@seek_speed[direction]
|
101
|
+
end
|
102
|
+
#change song position
|
103
|
+
def seek_position(position)
|
104
|
+
position *= 0.1
|
105
|
+
relative_position = position * duration
|
106
|
+
if relative_position < seconds_played
|
107
|
+
difference = seconds_played - relative_position
|
108
|
+
@player.rewind(difference)
|
109
|
+
elsif download_progress > (relative_position / duration) && relative_position > seconds_played
|
110
|
+
log download_progress
|
111
|
+
difference = relative_position - seconds_played
|
112
|
+
@player.forward(difference)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def rewind
|
117
|
+
@player.rewind(seek_speed(:rewind))
|
118
|
+
end
|
119
|
+
|
120
|
+
def forward
|
121
|
+
seconds = seek_speed(:forward)
|
122
|
+
|
123
|
+
if ((seconds + seconds_played) / duration) < download_progress
|
124
|
+
@player.forward(seconds)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def stop
|
129
|
+
@player.stop_stream
|
130
|
+
end
|
131
|
+
|
132
|
+
def start
|
133
|
+
@player.start_stream
|
134
|
+
end
|
135
|
+
|
136
|
+
def toggle
|
137
|
+
@player.toggle
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Soundcloud2000
|
2
|
+
module Models
|
3
|
+
class Playlist
|
4
|
+
|
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
|
+
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require_relative 'user'
|
2
|
+
|
3
|
+
module Soundcloud2000
|
4
|
+
module Models
|
5
|
+
class Track
|
6
|
+
|
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
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require_relative 'collection'
|
2
|
+
require_relative 'track'
|
3
|
+
require_relative 'playlist'
|
4
|
+
|
5
|
+
module Soundcloud2000
|
6
|
+
module Models
|
7
|
+
class TrackCollection < Collection
|
8
|
+
DEFAULT_LIMIT = 50
|
9
|
+
|
10
|
+
attr_reader :limit
|
11
|
+
attr_accessor :collection_to_load, :user, :playlist
|
12
|
+
|
13
|
+
def initialize(client)
|
14
|
+
super
|
15
|
+
@limit = DEFAULT_LIMIT
|
16
|
+
@collection_to_load = :recent
|
17
|
+
end
|
18
|
+
|
19
|
+
def size
|
20
|
+
@rows.size
|
21
|
+
end
|
22
|
+
|
23
|
+
def clear_and_replace
|
24
|
+
clear
|
25
|
+
load_more
|
26
|
+
events.trigger(:replace)
|
27
|
+
end
|
28
|
+
|
29
|
+
def load
|
30
|
+
clear
|
31
|
+
load_more
|
32
|
+
end
|
33
|
+
|
34
|
+
def load_more
|
35
|
+
unless @loaded
|
36
|
+
tracks = self.send(@collection_to_load.to_s + "_tracks")
|
37
|
+
@loaded = true if tracks.empty?
|
38
|
+
append tracks.map {|hash| Track.new hash }
|
39
|
+
@page += 1
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def favorites_tracks
|
44
|
+
return [] if @user.nil?
|
45
|
+
@client.get(@user.uri + '/favorites', offset: @limit * @page, limit: @limit)
|
46
|
+
end
|
47
|
+
|
48
|
+
def recent_tracks
|
49
|
+
@client.get('/tracks', offset: @page * limit, limit: @limit)
|
50
|
+
end
|
51
|
+
|
52
|
+
def user_tracks
|
53
|
+
return [] if @user.nil?
|
54
|
+
@client.get(@user.uri + '/tracks', offset: @limit * @page, limit: @limit)
|
55
|
+
end
|
56
|
+
|
57
|
+
def playlist_tracks
|
58
|
+
return [] if @playlist.nil?
|
59
|
+
@client.get(@playlist.uri + '/tracks', offset: @limit * @page, limit: @limit)
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Soundcloud2000
|
2
|
+
module Models
|
3
|
+
class User
|
4
|
+
|
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,22 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
module Soundcloud2000
|
4
|
+
module TimeHelper
|
5
|
+
HOUR = 1000 * 60 * 60
|
6
|
+
MINUTE = 1000 * 60
|
7
|
+
SECONDS = 1000
|
8
|
+
|
9
|
+
def self.duration(milliseconds)
|
10
|
+
parts = [
|
11
|
+
milliseconds / 1000 / 60 / 60, # hours
|
12
|
+
milliseconds / 1000 / 60 % 60, # minutes
|
13
|
+
milliseconds / 1000 % 60, # seconds
|
14
|
+
]
|
15
|
+
|
16
|
+
parts.shift if parts.first.zero?
|
17
|
+
|
18
|
+
[ parts.first, *parts[1..-1].map { |part| '%02d' % part } ].join('.')
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|