kappa 0.1.1.pre → 0.1.2.pre

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.
data/lib/kappa/base.rb ADDED
@@ -0,0 +1,9 @@
1
+ class Class
2
+ def module_class(sym)
3
+ parts = name.split('::')
4
+ parts[-1] = sym
5
+ parts.inject(Kernel) { |const, part|
6
+ const.const_get(part)
7
+ }
8
+ end
9
+ end
@@ -0,0 +1,140 @@
1
+ module Kappa
2
+ class ChannelBase
3
+ include IdEquality
4
+
5
+ def initialize(arg, connection)
6
+ @connection = connection
7
+
8
+ case arg
9
+ when Hash
10
+ parse(arg)
11
+ when String
12
+ json = @connection.get("channels/#{arg}")
13
+ parse(json)
14
+ else
15
+ raise ArgumentError
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ module Kappa::V2
22
+ # TODO:
23
+ # c.subscriptions
24
+ # c.start_commercial
25
+ # c.reset_stream_key
26
+ # c.foo = 'bar' ; c.save!
27
+ # Current user's channel
28
+ class Channel < Kappa::ChannelBase
29
+ def initialize(arg, connection = Connection.instance)
30
+ super(arg, connection)
31
+ end
32
+
33
+ def mature?
34
+ @mature
35
+ end
36
+
37
+ # TODO: Move these into derived classes?
38
+ def stream
39
+ # TODO: Use _links instead of hard-coding.
40
+ json = @connection.get("streams/#{@name}")
41
+ stream_json = json['stream']
42
+ Stream.new(stream_json, @connection)
43
+ end
44
+
45
+ def streaming?
46
+ stream.live?
47
+ end
48
+
49
+ #
50
+ # GET /channels/:channel/editors
51
+ # https://github.com/justintv/Twitch-API/blob/master/v2_resources/channels.md#get-channelschanneleditors
52
+ #
53
+ def editors
54
+ # TODO
55
+ end
56
+
57
+ #
58
+ # GET /channels/:channels/videos
59
+ # https://github.com/justintv/Twitch-API/blob/master/v2_resources/videos.md#get-channelschannelvideos
60
+ #
61
+ def videos(params = {})
62
+ # TODO
63
+ end
64
+
65
+ #
66
+ # GET /channels/:channel/follows
67
+ # https://github.com/justintv/Twitch-API/blob/master/v2_resources/channels.md#get-channelschannelfollows
68
+ # TODO: Warning: this set can be very large, this can run for very long time, recommend using :limit/:offset.
69
+ #
70
+ def followers(params = {})
71
+ limit = params[:limit] || 0
72
+
73
+ followers = []
74
+ ids = Set.new
75
+
76
+ @connection.paginated("channels/#{@name}/follows", params) do |json|
77
+ current_followers = json['follows']
78
+ current_followers.each do |follow_json|
79
+ user_json = follow_json['user']
80
+ user = User.new(user_json, @connection)
81
+ if ids.add?(user.id)
82
+ followers << user
83
+ if followers.count == limit
84
+ return followers
85
+ end
86
+ end
87
+ end
88
+
89
+ !current_followers.empty?
90
+ end
91
+
92
+ followers
93
+ end
94
+
95
+ # TODO: Requires authentication.
96
+ def subscribers
97
+ end
98
+
99
+ #
100
+ # GET /channels/:channel/subscriptions/:user
101
+ # https://github.com/justintv/Twitch-API/blob/master/v2_resources/subscriptions.md#get-channelschannelsubscriptionsuser
102
+ #
103
+ # TODO: Requires authentication.
104
+ def has_subscriber?(user)
105
+ # Support User object or username (string)
106
+ end
107
+
108
+ attr_reader :id
109
+ attr_reader :background_url
110
+ attr_reader :banner_url
111
+ attr_reader :created_at
112
+ attr_reader :stream_delay_sec
113
+ attr_reader :display_name
114
+ attr_reader :game_name
115
+ attr_reader :logo_url
116
+ attr_reader :name
117
+ attr_reader :status
118
+ attr_reader :updated_at
119
+ attr_reader :url
120
+ attr_reader :video_banner_url
121
+
122
+ private
123
+ def parse(hash)
124
+ @id = hash['_id']
125
+ @background_url = hash['background']
126
+ @banner_url = hash['banner']
127
+ @created_at = DateTime.parse(hash['created_at'])
128
+ @stream_delay_sec = hash['delay']
129
+ @display_name = hash['display_name']
130
+ @game_name = hash['game']
131
+ @logo_url = hash['logo']
132
+ @mature = hash['mature'] || false
133
+ @name = hash['name']
134
+ @status = hash['status']
135
+ @updated_at = DateTime.parse(hash['updated_at'])
136
+ @url = hash['url']
137
+ @video_banner_url = hash['video_banner']
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,119 @@
1
+ require 'httparty'
2
+ require 'addressable/uri'
3
+ require 'securerandom'
4
+ require 'json'
5
+
6
+ module Kappa
7
+ class ConnectionBase
8
+ include HTTParty
9
+ debug_output $stdout
10
+
11
+ def initialize(base_url = DEFAULT_BASE_URL)
12
+ @base_url = Addressable::URI.parse(base_url)
13
+
14
+ uuid = SecureRandom.uuid
15
+ # TODO: Use current library version.
16
+ @client_id = "Kappa-v1-#{uuid}"
17
+
18
+ @last_request_time = Time.now - RATE_LIMIT_SEC
19
+ end
20
+
21
+ def self.instance
22
+ @connection ||= self.class_eval do
23
+ self.new
24
+ end
25
+ end
26
+
27
+ def get(path, query = nil)
28
+ request_url = @base_url + path
29
+
30
+ # Handle non-JSON response
31
+ # Handle invalid JSON
32
+ # Handle non-200 codes
33
+
34
+ headers = {
35
+ 'Client-ID' => @client_id,
36
+ }.merge(custom_headers)
37
+
38
+ response = rate_limit do
39
+ self.class.get(request_url, :headers => headers, :query => query)
40
+ end
41
+
42
+ json = response.body
43
+ return JSON.parse(json)
44
+ end
45
+
46
+ def paginated(path, params = {})
47
+ limit = [params[:limit] || 100, 100].min
48
+ offset = params[:offset] || 0
49
+
50
+ path_uri = Addressable::URI.parse(path)
51
+ query = { 'limit' => limit, 'offset' => offset }
52
+ path_uri.query_values ||= {}
53
+ path_uri.query_values = path_uri.query_values.merge(query)
54
+
55
+ request_url = path_uri.to_s
56
+
57
+ params = params.dup
58
+ params.delete(:limit)
59
+ params.delete(:offset)
60
+
61
+ # TODO: Hande request retry.
62
+ loop do
63
+ json = get(request_url, params)
64
+
65
+ if json['error'] && (json['status'] == 503)
66
+ break
67
+ end
68
+
69
+ break if !yield(json)
70
+
71
+ links = json['_links']
72
+ next_url = links['next']
73
+
74
+ next_uri = Addressable::URI.parse(next_url)
75
+ offset = next_uri.query_values['offset'].to_i
76
+
77
+ total = json['_total']
78
+ break if total && (offset > total)
79
+
80
+ request_url = next_url
81
+ end
82
+ end
83
+
84
+ private
85
+ def rate_limit
86
+ delta = Time.now - @last_request_time
87
+ delay = [RATE_LIMIT_SEC - delta, 0].max
88
+
89
+ sleep delay if delay > 0
90
+
91
+ begin
92
+ return yield
93
+ ensure
94
+ @last_request_time = Time.now
95
+ end
96
+ end
97
+
98
+ RATE_LIMIT_SEC = 1
99
+ DEFAULT_BASE_URL = 'https://api.twitch.tv/kraken/'
100
+ end
101
+ end
102
+
103
+ module Kappa::V2
104
+ class Connection < Kappa::ConnectionBase
105
+ private
106
+ def custom_headers
107
+ { 'Accept' => 'application/vnd.twitchtv.v2+json' }
108
+ end
109
+ end
110
+ end
111
+
112
+ module Kappa::V3
113
+ class Connection < Kappa::ConnectionBase
114
+ private
115
+ def custom_headers
116
+ { 'Accept' => 'application/vnd.twitchtv.v3+json' }
117
+ end
118
+ end
119
+ end
data/lib/kappa/game.rb ADDED
@@ -0,0 +1,122 @@
1
+ module Kappa
2
+ class GameBase
3
+ include IdEquality
4
+
5
+ def initialize(arg)
6
+ case arg
7
+ when Hash
8
+ parse(arg)
9
+ else
10
+ raise ArgumentError
11
+ end
12
+ end
13
+ end
14
+
15
+ class GameSuggestionBase
16
+ include IdEquality
17
+
18
+ def initialize(arg)
19
+ case arg
20
+ when Hash
21
+ parse(arg)
22
+ else
23
+ raise ArgumentError
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ module Kappa::V2
30
+ class Game < Kappa::GameBase
31
+ attr_reader :id
32
+ attr_reader :name
33
+ attr_reader :giantbomb_id
34
+ attr_reader :box_images
35
+ attr_reader :logo_images
36
+ attr_reader :channel_count
37
+ attr_reader :viewer_count
38
+
39
+ private
40
+ def parse(hash)
41
+ @channel_count = hash['channels']
42
+ @viewer_count = hash['viewers']
43
+
44
+ game = hash['game']
45
+ @id = game['_id']
46
+ @name = game['name']
47
+ @giantbomb_id = game['giantbomb_id']
48
+ @box_images = Images.new(game['box'])
49
+ @logo_images = Images.new(game['logo'])
50
+ end
51
+ end
52
+
53
+ class GameSuggestion < Kappa::GameSuggestionBase
54
+ attr_reader :id
55
+ attr_reader :name
56
+ attr_reader :giantbomb_id
57
+ attr_reader :popularity
58
+ attr_reader :box_images
59
+ attr_reader :logo_images
60
+
61
+ private
62
+ def parse(hash)
63
+ @id = hash['_id']
64
+ @name = hash['name']
65
+ @giantbomb_id = hash['giantbomb_id']
66
+ @popularity = hash['popularity']
67
+ @box_images = Images.new(hash['box'])
68
+ @logo_images = Images.new(hash['logo'])
69
+ end
70
+ end
71
+
72
+ class Games
73
+ #
74
+ # GET /games/top
75
+ # https://github.com/justintv/Twitch-API/blob/master/v2_resources/games.md#get-gamestop
76
+ #
77
+ def top(params = {})
78
+ limit = params[:limit] || 0
79
+
80
+ games = []
81
+ ids = Set.new
82
+
83
+ @conn.paginated('games/top', params) do |json|
84
+ current_games = json['top']
85
+ current_games.each do |game_json|
86
+ game = Game.new(game_json)
87
+ if ids.add?(game.id)
88
+ games << game
89
+ if games.count == limit
90
+ return games
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ games
97
+ end
98
+
99
+ #
100
+ # GET /search/games
101
+ # https://github.com/justintv/Twitch-API/blob/master/v2_resources/search.md#get-searchgames
102
+ #
103
+ def search(params = {})
104
+ live = params[:live] || false
105
+ name = params[:name]
106
+
107
+ games = []
108
+ ids = Set.new
109
+
110
+ json = @conn.get('search/games', :query => name, :type => 'suggest', :live => live)
111
+ all_games = json['games']
112
+ all_games.each do |game_json|
113
+ game = GameSuggestion.new(game_json)
114
+ if ids.add?(game.id)
115
+ games << game
116
+ end
117
+ end
118
+
119
+ games
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,16 @@
1
+ module Kappa
2
+ module IdEquality
3
+ def hash
4
+ @id.hash
5
+ end
6
+
7
+ def eql?(other)
8
+ other && (self.class == other.class) && (self.id == other.id)
9
+ end
10
+
11
+ def ==(other)
12
+ eql?(other)
13
+ end
14
+ end
15
+ end
16
+
@@ -0,0 +1,33 @@
1
+ module Kappa
2
+ class ImagesBase
3
+ def initialize(arg)
4
+ case arg
5
+ when Hash
6
+ parse(arg)
7
+ else
8
+ raise ArgumentError
9
+ end
10
+ end
11
+ end
12
+ end
13
+
14
+ module Kappa::V2
15
+ class Images < Kappa::ImagesBase
16
+ def url(width, height)
17
+ @template_url.gsub('{width}', width.to_s, '{height}', height.to_s)
18
+ end
19
+
20
+ attr_reader :large_url
21
+ attr_reader :medium_url
22
+ attr_reader :small_url
23
+ attr_reader :template_url
24
+
25
+ private
26
+ def parse(hash)
27
+ @large_url = hash['large']
28
+ @medium_url = hash['medium']
29
+ @small_url = hash['small']
30
+ @template_url = hash['template']
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,151 @@
1
+ require 'set'
2
+
3
+ module Kappa
4
+ class StreamBase
5
+ include IdEquality
6
+
7
+ def initialize(hash, connection = self.class.default_connection)
8
+ @connection = connection
9
+ parse(hash)
10
+ end
11
+
12
+ def self.get(stream_name, connection = default_connection)
13
+ json = connection.get("streams/#{stream_name}")
14
+ stream = json['stream']
15
+ if json['status'] == 404 || !stream
16
+ nil
17
+ else
18
+ new(stream, connection)
19
+ end
20
+ end
21
+
22
+ private
23
+ def self.default_connection
24
+ self.class.module_class(:Connection).instance
25
+ end
26
+ end
27
+ end
28
+
29
+ module Kappa::V2
30
+ class Stream < Kappa::StreamBase
31
+ attr_reader :id
32
+ attr_reader :broadcaster
33
+ attr_reader :game_name
34
+ attr_reader :name
35
+ attr_reader :viewer_count
36
+ attr_reader :preview_url
37
+ attr_reader :channel
38
+
39
+ private
40
+ def parse(hash)
41
+ @id = hash['_id']
42
+ @broadcaster = hash['broadcaster']
43
+ @game_name = hash['game']
44
+ @name = hash['name']
45
+ @viewer_count = hash['viewers']
46
+ @preview_url = hash['preview']
47
+ @channel = Channel.new(hash['channel'], @connection)
48
+ end
49
+ end
50
+
51
+ class Streams
52
+ def self.all
53
+ end
54
+
55
+ #
56
+ # GET /streams
57
+ # https://github.com/justintv/Twitch-API/blob/master/v2_resources/streams.md
58
+ # :game (single, string), :channel (string array), :limit (int), :offset (int), :embeddable (bool), :hls (bool)
59
+ # TODO: Support Kappa::Vx::Game object for the :game param.
60
+ # TODO: Support Kappa::Vx::Channel object for the :channel param.
61
+ #
62
+ def self.where(args)
63
+ check = args.dup
64
+ check.delete(:limit)
65
+ check.delete(:offset)
66
+ raise ArgumentError if check.empty?
67
+
68
+ params = {}
69
+
70
+ if args[:channel]
71
+ params[:channel] = args[:channel].join(',')
72
+ end
73
+
74
+ if args[:game]
75
+ params[:game] = args[:game]
76
+ end
77
+
78
+ limit = args[:limit]
79
+ if limit && (limit < 25)
80
+ params[:limit] = limit
81
+ else
82
+ params[:limit] = 25
83
+ limit = 0
84
+ end
85
+
86
+ streams = []
87
+ ids = Set.new
88
+
89
+ connection = Connection.instance
90
+ connection.paginated('streams', params) do |json|
91
+ current_streams = json['streams']
92
+ current_streams.each do |stream_json|
93
+ stream = Stream.new(stream_json, connection)
94
+ if ids.add?(stream.id)
95
+ streams << stream
96
+ if streams.count == limit
97
+ return streams
98
+ end
99
+ end
100
+ end
101
+
102
+ !current_streams.empty?
103
+ end
104
+
105
+ streams
106
+ end
107
+
108
+ #
109
+ # GET /streams/featured
110
+ # https://github.com/justintv/Twitch-API/blob/master/v2_resources/streams.md#get-streamsfeatured
111
+ #
112
+ def self.featured(params = {})
113
+ limit = params[:limit] || 0
114
+
115
+ streams = []
116
+ ids = Set.new
117
+
118
+ connection = Connection.instance
119
+ connection.paginated('streams/featured', params) do |json|
120
+ current_streams = json['featured']
121
+ current_streams.each do |featured_json|
122
+ # TODO: Capture more information from the featured_json structure (need a FeaturedStream class?)
123
+ stream_json = featured_json['stream']
124
+ stream = Stream.new(stream_json, connection)
125
+ if ids.add?(stream.id)
126
+ streams << stream
127
+ if streams.count == limit
128
+ return streams
129
+ end
130
+ end
131
+ end
132
+
133
+ !current_streams.empty?
134
+ end
135
+
136
+ streams
137
+ end
138
+ end
139
+ end
140
+
141
+ module Kappa::V3
142
+ class Stream < Kappa::StreamBase
143
+ def initialize(arg, connection = Connection.instance)
144
+ super(arg, connection)
145
+ end
146
+
147
+ private
148
+ def parse(hash)
149
+ end
150
+ end
151
+ end