soundcloud2000 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
NTg4YWEyOTgxYjg2MTVjZGNhNGJkOGQ0ZDc5MDRhMDdlM2Y0YjUyZA==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
ZmZiYTAwMzUxZDM5MzRmMjMwOTRiYmI4OWFkYjgxZWUxMjJlOThiMQ==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
ZjcxZTM2NGFiMGRmZDM5M2ZlNTk5ZWNiNjFmODhmZjg5OTA0NGFiNjM1NGI3
|
10
|
+
OGI0NzI2ZDBmMDMyMzgzZGRhNTZhNmZjMmFmZTk4M2Y2YWVlZDJmN2Y2YmQ1
|
11
|
+
Y2Q4MmJkZGNmY2Q3NWI3MzdhOTlhZjc0MzRjMmYzNjRiZGI3YzA=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
ODQzOTBhZGFhNDlmYTU4MDY5N2EyOTY3NGQ1ZTc1MmUzZmRiZDQ1NmRlOTlj
|
14
|
+
ZjM5M2I4MzQxNDUwMjhmYWRlZmYwYzM3NjZiYWViZWZhMDdiMmVkYjIxMjZk
|
15
|
+
OTk0YmZkNzVlZmNlNjFmOWZmMzg0NGIwYTJiZTljMGU5MGVhZmI=
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
Copyright (c) 2013 Matthias Georgi and Tobias Schmidt
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
4
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
# soundcloud2000
|
2
|
+
|
3
|
+
The next generation SoundCloud client. Without all these stupid CSS files. Runs on OSX and Linux.
|
4
|
+
|
5
|
+
![Screen Shot 2013-01-20 at 15 37 03](https://f.cloud.github.com/assets/3432/81282/06c44c7e-630f-11e2-9a91-85c9b917835c.png)
|
6
|
+
![Screen Shot 2013-01-20 at 15 37 54](https://f.cloud.github.com/assets/3432/81281/06b05df4-630f-11e2-8b55-7f3c18126831.png)
|
7
|
+
|
8
|
+
This hack was built at the [Music Hack Day Stockholm 2013](http://stockholm.musichackday.org/2013).
|
9
|
+
|
10
|
+
## Requirements
|
11
|
+
|
12
|
+
* Ruby (1.9)
|
13
|
+
* Portaudio (19)
|
14
|
+
* Mpg123 (1.14)
|
15
|
+
|
16
|
+
## Installation
|
17
|
+
|
18
|
+
Assuming you have Ruby/Rubygems/Bundler installed, you need
|
19
|
+
portaudio and mpg123 as library to compile the native extensions.
|
20
|
+
|
21
|
+
### OSX
|
22
|
+
|
23
|
+
brew install portaudio
|
24
|
+
brew install mpg123
|
25
|
+
bundle install
|
26
|
+
bin/soundcloud2000
|
27
|
+
|
28
|
+
### Debian / Ubuntu
|
29
|
+
|
30
|
+
apt-get install portaudio19-dev libmpg123-dev
|
31
|
+
bundle install
|
32
|
+
bin/soundcloud2000
|
33
|
+
|
34
|
+
## Features
|
35
|
+
|
36
|
+
* stream SoundCloud tracks in your terminal (`enter`)
|
37
|
+
* scroll through sound lists (`down` / `up`)
|
38
|
+
* play / pause support (`space`)
|
39
|
+
* forward / rewind support (`right` / `left`)
|
40
|
+
* play tracks of different users (`u`)
|
41
|
+
* level meter
|
42
|
+
|
43
|
+
## Planned
|
44
|
+
|
45
|
+
* play any streams, sets or sounds
|
46
|
+
* better browsing between users and sound lists
|
47
|
+
|
48
|
+
## Authors
|
49
|
+
|
50
|
+
* [Matthias Georgi](https://github.com/georgi) ([@mgeorgi](https://twitter.com/mgeorgi))
|
51
|
+
* [Tobias Schmidt](https://github.com/grobie) ([@dagrobie](https://twitter.com/dagrobie))
|
52
|
+
|
53
|
+
## Contributors
|
54
|
+
|
55
|
+
* [Travis Thieman](https://github.com/tthieman) ([@tthieman](https://twitter.com/thieman))
|
data/Rakefile
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rake'
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
def dependencies(file)
|
6
|
+
`otool -L #{file}`.split("\n")[1..-1].map {|line| line.split[0] }
|
7
|
+
end
|
8
|
+
|
9
|
+
task :dirs do
|
10
|
+
Dir.mkdir 'vendor' rescue nil
|
11
|
+
Dir.mkdir 'vendor/bin' rescue nil
|
12
|
+
Dir.mkdir 'vendor/lib' rescue nil
|
13
|
+
Dir.mkdir 'vendor/dyld' rescue nil
|
14
|
+
end
|
15
|
+
|
16
|
+
task :ruby => :dirs do
|
17
|
+
lines = `rvm info`.split("\n")[6..-1]
|
18
|
+
ruby = YAML.load(lines.join("\n"))
|
19
|
+
|
20
|
+
version = ruby.keys.first
|
21
|
+
home = ruby[version]['homes']['ruby']
|
22
|
+
bin = ruby[version]['binaries']['ruby']
|
23
|
+
# dylib = dependencies(bin)[0]
|
24
|
+
|
25
|
+
`cp -a #{home}/lib/ruby/1.9.1/* vendor/lib`
|
26
|
+
`cp #{bin} vendor/`
|
27
|
+
# `cp #{dylib} vendor/dyld`
|
28
|
+
end
|
29
|
+
|
30
|
+
task :libs => :dirs do
|
31
|
+
[:audite, :portaudio, :mpg123].each do |name|
|
32
|
+
lib = `gem which #{name}`.chomp
|
33
|
+
`cp #{lib} vendor/lib`
|
34
|
+
if File.extname(lib) == '.bundle'
|
35
|
+
dylib = dependencies(lib)[0]
|
36
|
+
`cp #{dylib} vendor/dyld`
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
task :package => [:ruby, :libs] do
|
42
|
+
`cp soundcloud2000 vendor`
|
43
|
+
`cp -a bin/* vendor/bin`
|
44
|
+
`cp -a lib/* vendor/lib`
|
45
|
+
`tar czf soundcloud2000.tgz vendor`
|
46
|
+
end
|
data/bin/soundcloud2000
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require_relative 'soundcloud2000/client'
|
2
|
+
require_relative 'soundcloud2000/application'
|
3
|
+
|
4
|
+
module Soundcloud2000
|
5
|
+
CLIENT_ID = '29f8e018e1272c27bff7d510a10da2a8'
|
6
|
+
|
7
|
+
def self.start
|
8
|
+
client = Client.new(CLIENT_ID)
|
9
|
+
application = Application.new(client)
|
10
|
+
|
11
|
+
Signal.trap('SIGINT') do
|
12
|
+
application.stop
|
13
|
+
end
|
14
|
+
|
15
|
+
application.run
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
require_relative 'ui/canvas'
|
4
|
+
require_relative 'ui/input'
|
5
|
+
require_relative 'ui/rect'
|
6
|
+
|
7
|
+
require_relative 'controllers/track_controller'
|
8
|
+
require_relative 'controllers/player_controller'
|
9
|
+
|
10
|
+
require_relative 'models/track_collection'
|
11
|
+
require_relative 'models/player'
|
12
|
+
|
13
|
+
require_relative 'views/tracks_table'
|
14
|
+
require_relative 'views/splash'
|
15
|
+
|
16
|
+
module Soundcloud2000
|
17
|
+
class Application
|
18
|
+
include Controllers
|
19
|
+
include Models
|
20
|
+
include Views
|
21
|
+
|
22
|
+
def initialize(client)
|
23
|
+
$stderr.reopen("debug.log", "w")
|
24
|
+
@canvas = UI::Canvas.new
|
25
|
+
|
26
|
+
@splash_controller = Controller.new(Splash.new(
|
27
|
+
UI::Rect.new(0, 0, Curses.cols, Curses.lines)))
|
28
|
+
|
29
|
+
@player_controller = PlayerController.new(PlayerView.new(
|
30
|
+
UI::Rect.new(0, 0, Curses.cols, 5)), client)
|
31
|
+
|
32
|
+
@track_controller = TrackController.new(TracksTable.new(
|
33
|
+
UI::Rect.new(0, 5, Curses.cols, Curses.lines - 5)), client)
|
34
|
+
|
35
|
+
@track_controller.bind_to(TrackCollection.new(client))
|
36
|
+
|
37
|
+
@track_controller.events.on(:select) do |track|
|
38
|
+
@player_controller.play(track)
|
39
|
+
end
|
40
|
+
|
41
|
+
@player_controller.events.on(:complete) do
|
42
|
+
@track_controller.next_track
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def main
|
47
|
+
loop do
|
48
|
+
if @workaround_was_called_once_already
|
49
|
+
handle UI::Input.get(-1)
|
50
|
+
else
|
51
|
+
@workaround_was_called_once_already = true
|
52
|
+
handle UI::Input.get(0)
|
53
|
+
@track_controller.load
|
54
|
+
@track_controller.render
|
55
|
+
end
|
56
|
+
|
57
|
+
break if stop?
|
58
|
+
end
|
59
|
+
ensure
|
60
|
+
@canvas.close
|
61
|
+
end
|
62
|
+
|
63
|
+
def run
|
64
|
+
@splash_controller.render
|
65
|
+
main
|
66
|
+
end
|
67
|
+
|
68
|
+
# TODO: look at active controller and send key to active controller instead
|
69
|
+
def handle(key)
|
70
|
+
case key
|
71
|
+
when :left, :right, :space, :one, :two, :three, :four, :five, :six, :seven, :eight, :nine
|
72
|
+
@player_controller.events.trigger(:key, key)
|
73
|
+
when :down, :up, :enter, :u, :f, :s, :j, :k
|
74
|
+
@track_controller.events.trigger(:key, key)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def stop
|
79
|
+
@stop = true
|
80
|
+
end
|
81
|
+
|
82
|
+
def stop?
|
83
|
+
@stop == true
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.logger
|
87
|
+
@logger ||= Logger.new('debug.log')
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module Soundcloud2000
|
5
|
+
class Client
|
6
|
+
DEFAULT_LIMIT = 50
|
7
|
+
|
8
|
+
attr_reader :client_id
|
9
|
+
|
10
|
+
def initialize(client_id)
|
11
|
+
@client_id = client_id
|
12
|
+
end
|
13
|
+
|
14
|
+
def tracks(page = 1, limit = DEFAULT_LIMIT)
|
15
|
+
get('/tracks', offset: (page - 1) * limit, limit: limit)
|
16
|
+
end
|
17
|
+
|
18
|
+
def resolve(permalink)
|
19
|
+
res = get('/resolve', url: "http://soundcloud.com/#{permalink}")
|
20
|
+
if location = res['location']
|
21
|
+
get URI.parse(location).path
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def uri_escape(params)
|
26
|
+
URI.escape(params.collect{|k,v| "#{k}=#{v}"}.join('&'))
|
27
|
+
end
|
28
|
+
|
29
|
+
def request(type, path, params={})
|
30
|
+
params[:client_id] = client_id
|
31
|
+
params[:format] = 'json'
|
32
|
+
|
33
|
+
Net::HTTP.start('api.soundcloud.com', 443, :use_ssl => true) do |http|
|
34
|
+
http.request(type.new("#{path}?#{uri_escape params}"))
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def get(path, params={})
|
39
|
+
JSON.parse(request(Net::HTTP::Get, path, params).body)
|
40
|
+
end
|
41
|
+
|
42
|
+
def location(url)
|
43
|
+
uri = URI.parse(url)
|
44
|
+
res = request(Net::HTTP::Get, uri.path)
|
45
|
+
if res.code == '302'
|
46
|
+
res.header['Location']
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require_relative '../events'
|
2
|
+
|
3
|
+
module Soundcloud2000
|
4
|
+
module Controllers
|
5
|
+
class Controller
|
6
|
+
attr_reader :events
|
7
|
+
|
8
|
+
def initialize(view)
|
9
|
+
@view = view
|
10
|
+
@events = Events.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def render
|
14
|
+
@view.render
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require_relative 'controller'
|
2
|
+
require_relative '../models/player'
|
3
|
+
require_relative '../views/player_view'
|
4
|
+
|
5
|
+
module Soundcloud2000
|
6
|
+
module Controllers
|
7
|
+
class PlayerController < Controller
|
8
|
+
|
9
|
+
def initialize(view, client)
|
10
|
+
super(view)
|
11
|
+
|
12
|
+
@client = client
|
13
|
+
@player = Models::Player.new
|
14
|
+
|
15
|
+
@player.events.on(:progress) do
|
16
|
+
@view.render
|
17
|
+
end
|
18
|
+
|
19
|
+
@player.events.on(:complete) do
|
20
|
+
events.trigger(:complete)
|
21
|
+
end
|
22
|
+
|
23
|
+
@view.player(@player)
|
24
|
+
|
25
|
+
events.on(:key) do |key|
|
26
|
+
case key
|
27
|
+
when :left
|
28
|
+
@player.rewind
|
29
|
+
when :right
|
30
|
+
@player.forward
|
31
|
+
when :space
|
32
|
+
if @player.track
|
33
|
+
@player.toggle
|
34
|
+
@view.render
|
35
|
+
end
|
36
|
+
when :one
|
37
|
+
@player.seek_position(1)
|
38
|
+
when :two
|
39
|
+
@player.seek_position(2)
|
40
|
+
when :three
|
41
|
+
@player.seek_position(3)
|
42
|
+
when :four
|
43
|
+
@player.seek_position(4)
|
44
|
+
when :five
|
45
|
+
@player.seek_position(5)
|
46
|
+
when :six
|
47
|
+
@player.seek_position(6)
|
48
|
+
when :seven
|
49
|
+
@player.seek_position(7)
|
50
|
+
when :eight
|
51
|
+
@player.seek_position(8)
|
52
|
+
when :nine
|
53
|
+
@player.seek_position(9)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def play(track)
|
59
|
+
location = @client.location(track.stream_url)
|
60
|
+
|
61
|
+
@player.play(track, location)
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require_relative 'controller'
|
2
|
+
require_relative '../time_helper'
|
3
|
+
require_relative '../ui/table'
|
4
|
+
require_relative '../ui/input'
|
5
|
+
require_relative '../models/track_collection'
|
6
|
+
require_relative '../models/user'
|
7
|
+
|
8
|
+
module Soundcloud2000
|
9
|
+
module Controllers
|
10
|
+
class TrackController < Controller
|
11
|
+
|
12
|
+
def initialize(view, client)
|
13
|
+
super(view)
|
14
|
+
|
15
|
+
@client = client
|
16
|
+
|
17
|
+
events.on(:key) do |key|
|
18
|
+
case key
|
19
|
+
when :enter
|
20
|
+
@view.select
|
21
|
+
events.trigger(:select, current_track)
|
22
|
+
when :up, :k
|
23
|
+
@view.up
|
24
|
+
when :down, :j
|
25
|
+
@view.down
|
26
|
+
@tracks.load_more if @view.bottom?
|
27
|
+
when :u
|
28
|
+
@tracks.user = fetch_user_with_message('Change to SoundCloud user: ')
|
29
|
+
@tracks.collection_to_load = :user
|
30
|
+
@tracks.clear_and_replace
|
31
|
+
when :f
|
32
|
+
if @tracks.user.nil?
|
33
|
+
@tracks.user = fetch_user_with_message('Change to favorites from SoundCloud user: ')
|
34
|
+
end
|
35
|
+
@tracks.collection_to_load = :favorites
|
36
|
+
@tracks.clear_and_replace
|
37
|
+
when :s
|
38
|
+
@view.clear
|
39
|
+
if @tracks.user.nil?
|
40
|
+
@tracks.user = fetch_user_with_message('Change to SoundCloud user: ')
|
41
|
+
end
|
42
|
+
set = UI::Input.getstr('Change to SoundCloud playlist: ')
|
43
|
+
@tracks.playlist = Models::Playlist.new(@client.resolve(@tracks.user.permalink + '/sets/' + set))
|
44
|
+
@tracks.collection_to_load = :playlist
|
45
|
+
@tracks.clear_and_replace
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def fetch_user_with_message(message_to_display)
|
51
|
+
permalink = UI::Input.getstr(message_to_display)
|
52
|
+
user_hash = @client.resolve(permalink)
|
53
|
+
if user_hash
|
54
|
+
Models::User.new(user_hash)
|
55
|
+
else
|
56
|
+
nil
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def current_track
|
61
|
+
@tracks[@view.current]
|
62
|
+
end
|
63
|
+
|
64
|
+
def bind_to(tracks)
|
65
|
+
@tracks = tracks
|
66
|
+
@view.bind_to(tracks)
|
67
|
+
end
|
68
|
+
|
69
|
+
def load
|
70
|
+
@tracks.load
|
71
|
+
end
|
72
|
+
|
73
|
+
def next_track
|
74
|
+
@view.down
|
75
|
+
@view.select
|
76
|
+
events.trigger(:select, current_track)
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|