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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: fc1e32472f08a791aea41714cb6965266ab7fe62
4
+ data.tar.gz: f36df4610ae1d4148015399eb4b0671b3b7eb90a
5
+ SHA512:
6
+ metadata.gz: e58fa0fa517f910a43e083a4fe6435ebda76a3e409f39ce4257473048b45f74d8ad2cff44b9361f256c1c502b9868a687633be8261694bef06e19c08ce2a3419
7
+ data.tar.gz: 8d431bc98d51389dd2111fbdfb6d1ef9b95eaa20fb3204209d8a2030d4b4565f5d765547ffa700f88d3b8779d1c72cf85ab6f5f7def11ce61c64d5a831f50a84
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ .DS_Store
2
+ *~
3
+ *.log
4
+ Gemfile.lock
5
+ pkg/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in audite.gemspec
4
+ gemspec
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,65 @@
1
+ # soundcloud9000
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 installed, you need portaudio and mpg123 as
19
+ library to compile the native extensions.
20
+
21
+ ### OSX
22
+
23
+ xcode-select --install
24
+ brew install portaudio
25
+ brew install mpg123
26
+ gem install soundcloud9000
27
+
28
+ ### Debian / Ubuntu
29
+
30
+ apt-get install portaudio19-dev libmpg123-dev libncurses-dev ruby1.9.1-dev
31
+ gem install soundcloud9000
32
+
33
+ ## Usage
34
+
35
+ In order to use soundcloud9000, you need to [acquire a client credential for your application](http://soundcloud.com/you/apps/new). soundcloud9000 expects a valid client id to be set in the SC_CLIENT_ID environment variable.
36
+
37
+ You can either set this up in your `.bashrc` or equivalent or you can specify it on the command line:
38
+
39
+ SC_CLIENT_ID=YOUR_CLIENT_ID soundcloud9000
40
+
41
+ ## Features
42
+
43
+ * stream SoundCloud tracks in your terminal (`enter`)
44
+ * scroll through sound lists (`down` / `up`)
45
+ * play / pause support (`space`)
46
+ * forward / rewind support (`right` / `left`)
47
+ * play tracks of different users (`u`)
48
+ * play favorites from a user (`f`)
49
+ * play sets/playlists from a user (`s`)
50
+ * level meter
51
+
52
+ ## Planned
53
+
54
+ * play any streams, sets or sounds
55
+ * better browsing between users and sound lists
56
+
57
+ ## Authors
58
+
59
+ * [Matthias Georgi](https://github.com/georgi) ([@mgeorgi](https://twitter.com/mgeorgi))
60
+ * [Tobias Schmidt](https://github.com/grobie) ([@dagrobie](https://twitter.com/dagrobie))
61
+
62
+ ## Contributors
63
+
64
+ * [Travis Thieman](https://github.com/tthieman) ([@tthieman](https://twitter.com/thieman))
65
+ * [Sean Lewis](https://github.com/sophisticasean) ([@FricSean](https://twitter.com/fricsean))
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 soundcloud9000 vendor`
43
+ `cp -a bin/* vendor/bin`
44
+ `cp -a lib/* vendor/lib`
45
+ `tar czf soundcloud9000.tgz vendor`
46
+ end
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # use local gem if present
4
+ lib = File.expand_path('../lib', File.dirname(__FILE__))
5
+ $LOAD_PATH.unshift(lib) if File.directory?(lib)
6
+
7
+ require 'soundcloud9000'
8
+
9
+ Soundcloud9000.start
@@ -0,0 +1,22 @@
1
+ require_relative 'soundcloud9000/client'
2
+ require_relative 'soundcloud9000/application'
3
+
4
+ module Soundcloud9000
5
+
6
+ def self.start
7
+ unless client_id = ENV['SC_CLIENT_ID']
8
+ puts "You need to set SC_CLIENT_ID to a valid client ID"
9
+ exit 1
10
+ end
11
+
12
+ client = Client.new(client_id)
13
+ application = Application.new(client)
14
+
15
+ Signal.trap('SIGINT') do
16
+ application.stop
17
+ end
18
+
19
+ application.run
20
+ end
21
+
22
+ end
@@ -0,0 +1,93 @@
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 Soundcloud9000
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(
27
+ Splash.new(
28
+ UI::Rect.new(0, 0, Curses.cols, Curses.lines)))
29
+
30
+ @player_controller = PlayerController.new(
31
+ PlayerView.new(
32
+ UI::Rect.new(0, 0, Curses.cols, 5)), client)
33
+
34
+ @track_controller = TrackController.new(
35
+ TracksTable.new(
36
+ UI::Rect.new(0, 5, Curses.cols, Curses.lines - 5)), client)
37
+
38
+ @track_controller.bind_to(TrackCollection.new(client))
39
+
40
+ @track_controller.events.on(:select) do |track|
41
+ @player_controller.play(track)
42
+ end
43
+
44
+ @player_controller.events.on(:complete) do
45
+ @track_controller.next_track
46
+ end
47
+ end
48
+
49
+ def main
50
+ loop do
51
+ if @workaround_was_called_once_already
52
+ handle UI::Input.get(-1)
53
+ else
54
+ @workaround_was_called_once_already = true
55
+ handle UI::Input.get(0)
56
+ @track_controller.load
57
+ @track_controller.render
58
+ end
59
+
60
+ break if stop?
61
+ end
62
+ ensure
63
+ @canvas.close
64
+ end
65
+
66
+ def run
67
+ @splash_controller.render
68
+ main
69
+ end
70
+
71
+ # TODO: look at active controller and send key to active controller instead
72
+ def handle(key)
73
+ case key
74
+ when :left, :right, :space, :one, :two, :three, :four, :five, :six, :seven, :eight, :nine
75
+ @player_controller.events.trigger(:key, key)
76
+ when :down, :up, :enter, :u, :f, :s, :j, :k
77
+ @track_controller.events.trigger(:key, key)
78
+ end
79
+ end
80
+
81
+ def stop
82
+ @stop = true
83
+ end
84
+
85
+ def stop?
86
+ @stop == true
87
+ end
88
+
89
+ def self.logger
90
+ @logger ||= Logger.new('debug.log')
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,51 @@
1
+ require 'net/http'
2
+ require 'json'
3
+
4
+ module Soundcloud9000
5
+ # responsible for the very basic information of the app
6
+ class Client
7
+ DEFAULT_LIMIT = 50
8
+
9
+ attr_reader :client_id, :current_user
10
+ attr_writer :current_user
11
+
12
+ def initialize(client_id)
13
+ @client_id = client_id
14
+ @current_user = nil
15
+ end
16
+
17
+ def tracks(page = 1, limit = DEFAULT_LIMIT)
18
+ get('/tracks', offset: (page - 1) * limit, limit: limit)
19
+ end
20
+
21
+ def resolve(permalink)
22
+ res = get('/resolve', url: "http://soundcloud.com/#{permalink}")
23
+ if res['location']
24
+ get URI.parse(res['location']).path
25
+ end
26
+ end
27
+
28
+ def uri_escape(params)
29
+ URI.escape(params.collect { |k, v| "#{k}=#{v}" }.join('&'))
30
+ end
31
+
32
+ def request(type, path, params = {})
33
+ params[:client_id] = client_id
34
+ params[:format] = 'json'
35
+
36
+ Net::HTTP.start('api.soundcloud.com', 443, use_ssl: true) do |http|
37
+ http.request(type.new("#{path}?#{uri_escape params}"))
38
+ end
39
+ end
40
+
41
+ def get(path, params = {})
42
+ JSON.parse(request(Net::HTTP::Get, path, params).body)
43
+ end
44
+
45
+ def location(url)
46
+ uri = URI.parse(url)
47
+ res = request(Net::HTTP::Get, uri.path)
48
+ res.header['Location'] if res.code == '302'
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,19 @@
1
+ require_relative '../events'
2
+
3
+ module Soundcloud9000
4
+ module Controllers
5
+ # Control our view, events, and rendering.
6
+ class Controller
7
+ attr_reader :events
8
+
9
+ def initialize(view)
10
+ @view = view
11
+ @events = Events.new
12
+ end
13
+
14
+ def render
15
+ @view.render
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,75 @@
1
+ require_relative 'controller'
2
+ require_relative '../models/player'
3
+ require_relative '../views/player_view'
4
+
5
+ module Soundcloud9000
6
+ module Controllers
7
+ # The top section player controller
8
+ # Displays current track position
9
+ # Equalizer and track information
10
+ class PlayerController < Controller
11
+ def initialize(view, client)
12
+ super(view)
13
+
14
+ @client = client
15
+ @player = Models::Player.new
16
+
17
+ @player.events.on(:progress) do
18
+ @view.render
19
+ end
20
+
21
+ @player.events.on(:complete) do
22
+ events.trigger(:complete)
23
+ end
24
+
25
+ @view.player = @player
26
+
27
+ events.on(:key) do |key|
28
+ if @player.playing?
29
+ case key
30
+ when :left
31
+ @player.rewind
32
+ when :right
33
+ @player.forward
34
+ when :one
35
+ @player.seek_position(1)
36
+ when :two
37
+ @player.seek_position(2)
38
+ when :three
39
+ @player.seek_position(3)
40
+ when :four
41
+ @player.seek_position(4)
42
+ when :five
43
+ @player.seek_position(5)
44
+ when :six
45
+ @player.seek_position(6)
46
+ when :seven
47
+ @player.seek_position(7)
48
+ when :eight
49
+ @player.seek_position(8)
50
+ when :nine
51
+ @player.seek_position(9)
52
+ end
53
+ end
54
+ if key == :space
55
+ if @player.track
56
+ @player.toggle
57
+ @view.render
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ def play(track)
64
+ if track.nil?
65
+ UI::Input.error('No track currently selected. Use f to switch to '\
66
+ "#{@client.current_user.username}'s favorites, or"\
67
+ ' s to switch to their playlists/sets.')
68
+ else
69
+ location = @client.location(track.stream_url)
70
+ @player.play(track, location)
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,90 @@
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 Soundcloud9000
9
+ module Controllers
10
+ # Handles the navigation thru the current track list
11
+ class TrackController < Controller
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
+ user = fetch_user_with_message('Change to soundcloud user: ')
29
+ unless user.nil?
30
+ @client.current_user = user
31
+ @tracks.collection_to_load = :user
32
+ @tracks.clear_and_replace
33
+ end
34
+ when :f
35
+ @client.current_user = fetch_user_with_message('Change to SoundCloud user\'s favourites: ') if @client.current_user.nil?
36
+ unless @client.current_user.nil?
37
+ @tracks.collection_to_load = :favorites
38
+ @tracks.clear_and_replace
39
+ end
40
+ when :s
41
+ @view.clear
42
+ @client.current_user = fetch_user_with_message('Change to SoundCloud user: ') if @client.current_user.nil?
43
+ unless @client.current_user.nil?
44
+ set = UI::Input.getstr('Change to SoundCloud playlist: ')
45
+ set_request = @client.resolve(@client.current_user.permalink + '/sets/' + set)
46
+ if set_request.nil?
47
+ UI::Input.error("No such set/playlist '#{set}' for #{@client.current_user.username}")
48
+ @client.current_user = nil
49
+ else
50
+ @tracks.playlist = Models::Playlist.new(set_request)
51
+ @tracks.collection_to_load = :playlist
52
+ @tracks.clear_and_replace
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ def fetch_user_with_message(message_to_display)
60
+ permalink = UI::Input.getstr(message_to_display)
61
+ user_hash = @client.resolve(permalink)
62
+ if user_hash
63
+ Models::User.new(user_hash)
64
+ else
65
+ UI::Input.error("No such user '#{permalink}'. Use u to try again.")
66
+ nil
67
+ end
68
+ end
69
+
70
+ def current_track
71
+ @tracks[@view.current]
72
+ end
73
+
74
+ def bind_to(tracks)
75
+ @tracks = tracks
76
+ @view.bind_to(tracks)
77
+ end
78
+
79
+ def load
80
+ @tracks.load
81
+ end
82
+
83
+ def next_track
84
+ @view.down
85
+ @view.select
86
+ events.trigger(:select, current_track)
87
+ end
88
+ end
89
+ end
90
+ end