twitchy 0.1.1

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 31af3d06423eafae96cc9dbf8b82607916ce868d
4
+ data.tar.gz: 665136e25a38df7a546bc6f9be18e568bfdc5fb3
5
+ SHA512:
6
+ metadata.gz: 4c8dc487aa64b1cb843c42f742014d0c27f7c3f53feace988bd4864edf44716043f89d8843f270ac02f9544373d5e9015b861b1a4d622e0f75152befa27e8ef4
7
+ data.tar.gz: 742a133b912a3bc5b290d301fafd219285856820fd8ce3e618c23aa8e9b2f4d97fa6c90df5a4bbbd80901303cf44166dd88d6cf0b63fc496f0ad40a5163a8758
@@ -0,0 +1,28 @@
1
+ # twitchy
2
+
3
+ A little Ruby wrapper around livestreamer
4
+
5
+ ## Documentation
6
+
7
+ ```
8
+ Usage: twitchy [options] [channel ..]
9
+ -p, --player PLAYER Set video player
10
+ -c, --chat Open popout chat with the stream
11
+ -u, --user USER Show subs for a user
12
+ -q, --quality QUALITY Set desired quality
13
+ -v, --videos List archives instead of streams
14
+ -l, --limit LIMIT Number to fetch at once
15
+ -g, --game GAME Add top streamers for GAME
16
+ --highlights Only show highlights when fetching videos
17
+ -h, --help Show this message
18
+ ```
19
+ ## Dependencies
20
+
21
+ Requires [`livestreamer`](https://github.com/chrippa/livestreamer) (obviously), and currently the [`colorize`](https://github.com/fazibear/colorize) gem.
22
+
23
+ ## Todo
24
+
25
+ I plan on gemifying this soon. Also, a lot of usage is unclear (pagination in
26
+ archives, quality options, https://github.com/fazibear/colorizeetc).
27
+
28
+ I have greatly reduced API requests from earlier versions, but I'm always keeping an eye out for ways to bring the number of those calls down further. There's a way to get all the videos from subscriptions of a user, but it requires authentication and isn't very flexible, for example, so right now I'm making calls for each requested channel.
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'twitchy/twitchy'
4
+
5
+ trap ("SIGINT") { exit! }
6
+
7
+ t = Twitchy.new(ARGV)
8
+
9
+ if t.streamers.empty?
10
+ if t.options.user
11
+ puts "User follows no streamers"
12
+ else
13
+ puts t.banner
14
+ end
15
+ exit
16
+ end
17
+
18
+ if t.options.videos
19
+ t.get_archives
20
+ else
21
+ t.check_status
22
+ if t.online.empty?
23
+ puts "All streamers are offline"
24
+ exit
25
+ end
26
+ end
27
+
28
+ t.puts_streams
29
+ t.get_choice
30
+
31
+ #vim: ft=rb
@@ -0,0 +1,29 @@
1
+ require 'ostruct'
2
+
3
+ class DeepStruct < OpenStruct
4
+ def initialize(hash=nil)
5
+ @table = {}
6
+ @hash_table = {}
7
+
8
+ if hash
9
+ hash.each do |k,v|
10
+ @table[k.to_sym] = v
11
+
12
+ @table[k.to_sym] = v.map do |o|
13
+ o.is_a?(Hash) ? self.class.new(o) : o
14
+ end if v.is_a? Array
15
+
16
+ @table[k.to_sym] = self.class.new(v) if v.is_a? Hash
17
+
18
+ @hash_table[k.to_sym] = v
19
+
20
+ new_ostruct_member(k)
21
+ end
22
+ end
23
+ end
24
+
25
+ def to_h
26
+ @hash_table
27
+ end
28
+
29
+ end
@@ -0,0 +1,28 @@
1
+ require 'json'
2
+
3
+ module Livestreamer
4
+ @@sort = ["best","source","high","medium","low","mobile","worst"]
5
+
6
+ def self.get_available_streams(channel)
7
+ @@sort & JSON.parse(`livestreamer -j twitch.tv/#{channel}`)["streams"].keys
8
+ end
9
+
10
+ def self.start_stream(channel, player, quality, chat)
11
+ stream = "livestreamer -Q -p '#{player}' "\
12
+ "twitch.tv/#{channel} #{quality} & "
13
+ popout_chat = "firefox -new-window "\
14
+ "'http://www.twitch.tv/chat/embed"\
15
+ "?channel=#{channel}&popout_chat=true' "\
16
+ "&> /dev/null & "
17
+ cmd = stream
18
+ cmd += popout_chat if chat
19
+
20
+ exec cmd
21
+ end
22
+
23
+ def self.start_video(url, player)
24
+ exec "livestreamer -Q -p '#{player}' "\
25
+ "#{url} best &"
26
+ end
27
+ end
28
+
@@ -0,0 +1,72 @@
1
+ require 'open-uri'
2
+ require 'twitchy/dstruct'
3
+ require 'json'
4
+
5
+ module TwitchAPI
6
+ @@PREFIX = "https://api.twitch.tv/kraken/"
7
+
8
+ def self.get_follow_data(user)
9
+ catch_exception(OpenURI::HTTPError) do
10
+ get_as_struct("users/#{user}/follows/channels").follows
11
+ end
12
+ end
13
+
14
+ def self.get_stream_status(streamers)
15
+ query = "streams?channel=#{streamers.join(",")}"
16
+ catch_exception(OpenURI::HTTPError, default: {}) do
17
+ get_as_struct(query).streams.map do |s|
18
+ [s.channel.name, s]
19
+ end.to_h
20
+ end
21
+ end
22
+
23
+ def self.get_stream_data(streamer)
24
+ catch_exception(OpenURI::HTTPError) do
25
+ get_as_struct("streams/#{streamer}").stream
26
+ end
27
+ end
28
+
29
+ def self.get_videos(streamer, limit: 10, offset: 0, highlights: false)
30
+ catch_exception(OpenURI::HTTPError) do
31
+ query = "channels/#{streamer}/videos"\
32
+ "?broadcasts=#{!highlights}"\
33
+ "&limit=#{limit}&offset=#{offset}"
34
+ get_as_struct(query).videos.each do |v|
35
+ v.recorded_at = DateTime.parse(v.recorded_at)
36
+ end
37
+ end
38
+ end
39
+
40
+ def self.get_streamers_for_game(game, limit: 10, offset: 0)
41
+ catch_exception(OpenURI::HTTPError) do
42
+ query = "streams?game=#{URI.encode(game)}&limit=#{limit}&offset=#{offset}"
43
+ get_as_struct(query).streams.map{|s| s.channel.name}
44
+ end
45
+ end
46
+
47
+ private
48
+ def self.catch_exception(exception, default: nil, &block)
49
+ if block
50
+ begin
51
+ yield
52
+ rescue exception
53
+ default
54
+ end
55
+ else
56
+ default
57
+ end
58
+ end
59
+
60
+ def self.get(request)
61
+ open( @@PREFIX + request ).each_line.to_a.join
62
+ end
63
+
64
+ def self.get_as_json(request)
65
+ JSON.parse(get(request))
66
+ end
67
+
68
+ def self.get_as_struct(request)
69
+ DeepStruct.new(get_as_json(request))
70
+ end
71
+
72
+ end
@@ -0,0 +1,278 @@
1
+ require 'colorize'
2
+ require 'optparse'
3
+ require 'ostruct'
4
+ require 'twitchy/livestreamer'
5
+ require 'twitchy/twitch_api'
6
+
7
+ class Twitchy
8
+ attr_reader :options, :streamers, :online
9
+
10
+ def initialize(args)
11
+ parse_options(args)
12
+
13
+ @streamers = args.map{|s| s.dup}
14
+ @streamers += fetch_subs(@options.user) if @options.user
15
+ @streamers += TwitchAPI.get_streamers_for_game(
16
+ @options.game, limit: @options.limit
17
+ ) if @options.game
18
+ end
19
+
20
+ def parse_options(args)
21
+ @options = OpenStruct.new(
22
+ player: "vlc --no-video-title-show",
23
+ quality: "best",
24
+ chat: false,
25
+ videos: false,
26
+ limit: 20,
27
+ highlights: false,
28
+ game: nil
29
+ )
30
+ @optparser = OptionParser.new do |opts|
31
+ opts.banner = "Usage: twitchy [options] [channel ..]"
32
+
33
+ opts.on("-p", "--player PLAYER", "Set video player") do |p|
34
+ @options.player = p
35
+ end
36
+
37
+ opts.on("-c", "--chat", "Open popout chat with the stream") do
38
+ @options.chat = true
39
+ end
40
+
41
+ opts.on("-u", "--user USER", "Show subs for a user") do |u|
42
+ @options.user = u
43
+ end
44
+
45
+ opts.on("-q", "--quality QUALITY", "Set desired quality") do |q|
46
+ @options.quality = q
47
+ end
48
+
49
+ opts.on("-v", "--videos", "List archives instead of streams") do
50
+ @options.videos = true
51
+ end
52
+
53
+ opts.on("-l", "--limit LIMIT", Integer, "Number to fetch at once") do |l|
54
+ @options.limit = l
55
+ end
56
+
57
+ opts.on("-g", "--game GAME", "Add top streamers for GAME") do |g|
58
+ @options.game = g
59
+ end
60
+
61
+ opts.on("--highlights", "Only show highlights when fetching videos") do
62
+ @options.highlights = true
63
+ end
64
+
65
+ opts.on_tail("-h", "--help", "Show this message") do
66
+ puts opts
67
+ exit
68
+ end
69
+ end
70
+
71
+ @optparser.parse!(args)
72
+ end
73
+
74
+ def fetch_subs(user)
75
+ puts "Fetching subscriptions".bold
76
+ TwitchAPI.get_follow_data(user).map do |follow|
77
+ follow.channel.name
78
+ end.reverse!
79
+ end
80
+
81
+ def check_status
82
+ puts "Checking streamer status...".bold
83
+ @online = TwitchAPI.get_stream_status(@streamers)
84
+ @online.each do |streamer, data|
85
+ print "Getting quality options for #{streamer} "
86
+ data.streams = Livestreamer.get_available_streams(streamer)
87
+ clearln
88
+ end
89
+ @online_streamers = @online.keys.sort_by!{|s| @online[s].viewers}.reverse!
90
+ end
91
+
92
+ def get_archives
93
+ puts "Requesting archives".bold
94
+ @archives = []
95
+ @archive_count = Hash.new(0)
96
+ streamers.each do |streamer|
97
+ print streamer
98
+ vods = TwitchAPI.get_videos(
99
+ streamer, limit: @options.limit, highlights: @options.highlights
100
+ )
101
+ @archives += vods
102
+ @archive_count[streamer] += vods.to_a.size
103
+ clearln
104
+ puts streamer.green
105
+ end
106
+ end
107
+
108
+
109
+ def banner
110
+ @optparser.help()
111
+ end
112
+
113
+ def clearln
114
+ print "\r\e[K"
115
+ end
116
+
117
+ def puts_streams(offset: 0)
118
+ puts
119
+ puts "Streams available:".bold
120
+ index = 0
121
+
122
+ if @options.videos
123
+ @archives.sort_by!{|v| v.recorded_at}.reverse!
124
+ @archives.drop(offset).take(@options.limit).each do |video|
125
+ streamer = video.channel.display_name
126
+ index += 1
127
+ puts_stream_info(index, streamer, video.title.to_s, video.game.to_s, time: video.length)
128
+ end
129
+ else
130
+ @online_streamers.each do |streamer|
131
+ info = @online[streamer].channel
132
+ streamer_name = info.display_name
133
+ index += 1
134
+ qualities = @online[streamer].streams
135
+ qualities = nil if qualities.include? @options.quality
136
+ puts_stream_info(index, streamer_name, info.status.to_s, info.game.to_s, qualities: qualities)
137
+ end
138
+ end
139
+ end
140
+
141
+ def puts_stream_info(index, streamer, title, game, qualities: nil, time: nil)
142
+ def abbrev(str, length=77)
143
+ short = str.chomp[0...length]
144
+ short += "..." if str.length > 77
145
+ short
146
+ end
147
+ title = abbrev(title)
148
+ puts "#{index.to_s.bold}. #{streamer} - "\
149
+ "#{"(#{format_time(time)}) " if time}"\
150
+ "'#{title.sub(game, game.underline)}' "\
151
+ "#{"playing #{game.underline}" unless title.include? game if game}"
152
+ puts " [ #{qualities.map{ |q|
153
+ if q == @options.quality then q.bold else q end
154
+ }.join(' | ')} ]" if qualities
155
+ end
156
+
157
+ def get_choice
158
+ if @options.videos
159
+ get_video_choice
160
+ else
161
+ get_stream_choice
162
+ end
163
+ end
164
+
165
+ def get_stream_choice
166
+ prompt = "Select stream number [and quality] to watch: ".bold
167
+ print prompt
168
+
169
+ loop do
170
+ response = ($stdin.gets || "").chomp.split
171
+
172
+ response, quality = case response.length
173
+ when 0
174
+ ["", nil]
175
+ when 1
176
+ [response[0], @options.quality]
177
+ else
178
+ response[0,2]
179
+ end
180
+
181
+ break if response.downcase == "q"
182
+
183
+ choice = Integer(response) rescue nil
184
+ if choice && choice > 0 && choice <= @online_streamers.length
185
+ streamer = @online_streamers[choice - 1]
186
+ if @online[streamer].streams.include? quality
187
+ launch_stream(streamer, quality)
188
+ else
189
+ print "Requested quality not available, options are "
190
+ puts "[#{@online[streamer].streams.join(", ")}]"
191
+ end
192
+ else
193
+ print "Invalid choice, try again"
194
+ end
195
+
196
+ #print "\r\e[A\e[K\r" + prompt
197
+ print prompt
198
+ end
199
+ end
200
+
201
+ def get_video_choice
202
+ prompt = "Select stream number to watch: ".bold
203
+ print prompt
204
+ offset = 0
205
+ streamer_offset = Hash.new{0}
206
+ loop do
207
+ response = $stdin.gets.chomp
208
+
209
+ break if response.downcase == "q"
210
+ if response == ":n" or response == ":next"
211
+ puts "Getting next page".bold
212
+ streamer_count = @archives.drop(offset).take(@options.limit).group_by{|v| v.channel.name}.map{|k,v| [k, v.size]}
213
+ offset += @options.limit
214
+ streamer_count.each do |s, o|
215
+ streamer_offset[s] += o
216
+ if streamer_offset[s] + @options.limit > @archive_count[s]
217
+ @archives += TwitchAPI.get_videos(
218
+ s, limit: o, offset: streamer_offset[s]
219
+ )
220
+ @archive_count[s] += o
221
+ end
222
+ end
223
+ puts_streams(offset: offset)
224
+ print prompt
225
+ next
226
+ end
227
+
228
+ if response == ":b" or response == ":back"
229
+ puts "Getting previous page".bold
230
+ streamer_count = @archives.drop(offset).take(@options.limit).group_by{|v| v.channel.name}.map{|k,v| [k, v.size]}
231
+ streamer_count.each do |s, o|
232
+ streamer_offset[s] -= o
233
+ streamer_offset[s] = 0 if streamer_offset[s] < 0
234
+ end
235
+ offset -= @options.limit
236
+ offset = 0 if offset < 0
237
+ puts_streams(offset: offset)
238
+ print prompt
239
+ next
240
+ end
241
+
242
+ choice = Integer(response) rescue nil
243
+ if choice && 0 < choice && choice <= 20
244
+ stream = @archives[offset + choice - 1]
245
+ launch_video(stream.url)
246
+ else
247
+ print "Invalid choice, try again"
248
+ end
249
+
250
+ print "\r\e[A\e[K\r" + prompt
251
+ end
252
+ end
253
+
254
+ def launch_stream(streamer, quality)
255
+ opts = @options.dup
256
+ opts.quality = quality
257
+ Livestreamer.start_stream(
258
+ streamer, @options.player, @options.quality, @options.chat
259
+ )
260
+ end
261
+
262
+ def launch_video(url)
263
+ Livestreamer.start_video(url, @options.player)
264
+ end
265
+
266
+ def format_time(seconds)
267
+ s = seconds % 60
268
+ minutes = seconds / 60
269
+ m = minutes % 60
270
+ h = minutes / 60
271
+ if h > 0
272
+ "%d:%02d:%02d" % [h,m,s]
273
+ else
274
+ "%d:%02d" % [m,s]
275
+ end
276
+ end
277
+ end
278
+
@@ -0,0 +1,24 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'twitchy'
3
+ s.version = '0.1.1'
4
+ s.date = '2014-02-11'
5
+ s.summary = "A Ruby wrapper around livestreamer"
6
+ s.description = "Twitchy provides for a system to query the TwitchAPI "\
7
+ "for use with livestreamer. List online channels, "\
8
+ "archived videos, and more."
9
+ s.authors = ["blkbsstt"]
10
+ s.email = 'blkbsstt+twitchy@gmail.com'
11
+ s.executables << 'twitchy'
12
+ s.files = [
13
+ "README.md",
14
+ "twitchy.gemspec",
15
+ "lib/twitchy/twitchy.rb",
16
+ "lib/twitchy/livestreamer.rb",
17
+ "lib/twitchy/dstruct.rb",
18
+ "lib/twitchy/twitch_api.rb",
19
+ "bin/twitchy"
20
+ ]
21
+ s.add_runtime_dependency "colorize", "~> 0.6"
22
+ s.homepage = 'http://www.github.com/blkbsstt/twitchy'
23
+ s.license = 'MIT'
24
+ end
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: twitchy
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - blkbsstt
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-02-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: colorize
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.6'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.6'
27
+ description: Twitchy provides for a system to query the TwitchAPI for use with livestreamer.
28
+ List online channels, archived videos, and more.
29
+ email: blkbsstt+twitchy@gmail.com
30
+ executables:
31
+ - twitchy
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - README.md
36
+ - bin/twitchy
37
+ - lib/twitchy/dstruct.rb
38
+ - lib/twitchy/livestreamer.rb
39
+ - lib/twitchy/twitch_api.rb
40
+ - lib/twitchy/twitchy.rb
41
+ - twitchy.gemspec
42
+ homepage: http://www.github.com/blkbsstt/twitchy
43
+ licenses:
44
+ - MIT
45
+ metadata: {}
46
+ post_install_message:
47
+ rdoc_options: []
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ requirements: []
61
+ rubyforge_project:
62
+ rubygems_version: 2.2.2
63
+ signing_key:
64
+ specification_version: 4
65
+ summary: A Ruby wrapper around livestreamer
66
+ test_files: []