summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--README.md2
-rw-r--r--src/twitcr/mappings/game.cr21
-rw-r--r--src/twitcr/rest.cr58
-rw-r--r--util/twitch.cr591
5 files changed, 669 insertions, 4 deletions
diff --git a/.gitignore b/.gitignore
index 0bbd4a9..1426189 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@
# Libraries don't need dependency lock
# Dependencies will be locked in applications that use them
/shard.lock
+/util/twitch
diff --git a/README.md b/README.md
index 04ea7f1..172ff0d 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# twitcr
-Very limited API Rest wrapper for interacting with twitch. Not planning on finishing.
+API wrapper for interacting with Twitch.
## Installation
diff --git a/src/twitcr/mappings/game.cr b/src/twitcr/mappings/game.cr
new file mode 100644
index 0000000..2915eb5
--- /dev/null
+++ b/src/twitcr/mappings/game.cr
@@ -0,0 +1,21 @@
+require "./converters"
+
+module Twitcr
+ struct GameList
+ JSON.mapping({data: Array(Game)})
+ end
+
+ struct Game
+ JSON.mapping({
+ id: {type: UInt64, converter: ID::Converter},
+ name: String,
+ box_art_url: String,
+ })
+ end
+end
+
+module ID::Converter
+ def self.from_json(value : JSON::PullParser) : UInt64
+ UInt64.new(value.read_string)
+ end
+end
diff --git a/src/twitcr/rest.cr b/src/twitcr/rest.cr
index f3af85c..0fc59c1 100644
--- a/src/twitcr/rest.cr
+++ b/src/twitcr/rest.cr
@@ -14,6 +14,7 @@ module Twitcr::REST
Api # typically undocumented APIs go here
Kraken # v5, "deprecated" with no real replacement for half of the functions
Helix # "new api"
+ Oauth2 # id
PubSub # FIXME: put this in another module, probably
end
@@ -22,10 +23,12 @@ module Twitcr::REST
headers = HTTP::Headers.new
headers["Client-ID"] = @settings["client_id"].not_nil!
+ url_base = API_BASE
api = twitchapi.to_s.downcase
case api
when "helix"
+ headers["Client-ID"] = @settings["client_id"].not_nil!
# FIXME: maybe don't send this every time?
headers["Authorization"] = "Bearer #{@settings["access_token"].not_nil!}"
when "kraken"
@@ -34,6 +37,17 @@ module Twitcr::REST
when "api"
# typically twitch restricts these to the twitch client id
headers["Client-ID"] = @settings["client_id_twitch"].not_nil!
+ when "oauth2"
+ url_base = "https://id.twitch.tv/"
+ headers["Authorization"] = "OAuth #{@settings["app_access_token"].not_nil!}"
+ else
+ end
+
+ case route
+ when /^\/validate/
+ headers["Authorization"] = "OAuth #{@settings["app_access_token"].not_nil!}"
+ when /^\/eventsub/
+ headers["Authorization"] = "Bearer #{@settings["app_access_token"].not_nil!}"
else
end
@@ -45,14 +59,14 @@ module Twitcr::REST
ENV["DEBUG"]
rescue KeyError
else
- puts method + " " + API_BASE + api + route
+ puts method + " " + url_base + api + route
#pp headers# probably too sensitive
pp body
end
response = HTTP::Client.exec(
method,
- API_BASE + api + route,
+ url_base + api + route,
headers,
body,
tls: SSL_CONTEXT
@@ -93,6 +107,16 @@ module Twitcr::REST
if ! query_string.empty?; query_string = "?" + query_string end
request( "GET", Api::Helix, "/users/follows" + query_string )
end
+ def get_user_follows( *, to : UInt64, from : UInt64, after : String? = nil )
+ params = Hash( String, String ).new
+ params["first"] = "100"
+ params["from_id"] = from.to_s
+ params["to_id"] = to.to_s
+ if after ; params["after"] = after end
+ query_string = HTTP::Params.encode( params )
+ if ! query_string.empty?; query_string = "?" + query_string end
+ request( "GET", Api::Helix, "/users/follows" + query_string )
+ end
# FIXME: support array of mixed UInt64/String
def get_games( name : String ); request( "GET", Api::Helix, "/games?name=#{URI.encode_www_form( name )}" ) end
def get_games( id : UInt64 ); request( "GET", Api::Helix, "/games?id=#{id.to_s}" ) end
@@ -125,6 +149,7 @@ module Twitcr::REST
def get_banned( broadcaster_id : UInt64, user_id : UInt64 ); request( "GET", Api::Helix, "/moderation/banned?broadcaster_id=#{broadcaster_id.to_s}&user_id=#{user_id}" ) end
def get_banned( broadcaster_id : UInt64, user_id : Array( UInt64 ) ); request( "GET", Api::Helix, "/moderation/banned?broadcaster_id=#{broadcaster_id.to_s}&user_id=#{user_id.join("&user_id=")}" ) end
def get_banned_events( broadcaster_id : UInt64, first : UInt64 = 100 ); request( "GET", Api::Helix, "/moderation/banned/events?broadcaster_id=#{broadcaster_id.to_s}&first=#{first.to_s}" ) end
+ def get_clips( name : String ); request( "GET", Api::Kraken, "/clips/top?channel=#{name}&first=100&period=all" ) end
def get_videos( id : UInt64, sort : String = "time", type : String = "all" ); request( "GET", Api::Helix, "/videos?user_id=#{id.to_s}&first=100&sort=#{sort}&type=#{type}" ) end
def get_channels_videos( id : UInt64, sort : String = "time", type : String = "all" ); request( "GET", Api::Kraken, "/channels/#{id.to_s}/videos?limit=100&sort=#{sort}&broadcast_type=#{type}" ) end
def get_channel( ); request( "GET", Api::Kraken, "/channel" ) end
@@ -139,7 +164,34 @@ module Twitcr::REST
def delete_key!( id : UInt64); request( "DELETE", Api::Kraken, "/channels/#{id.to_s}/stream_key" ) end
def get_hosts( id : UInt64 ); request( "GET", Api::Kraken, "/channels/#{id.to_s}/hosts" ) end
def get_subs( id : UInt64 ); request( "GET", Api::Kraken, "/channels/#{id.to_s}/subscriptions" ) end
-
+ def get_eventsubs( ); request( "GET", Api::Helix, "/eventsub/subscriptions" ) end
+ def delete_eventsubs!( id : String ); request( "DELETE",
+ Api::Helix, "/eventsub/subscriptions?id=#{id}" ) end
+ def put_eventsubs!( type : String, id : UInt64, url : String );
+ version = "1"
+ varname = "broadcaster_user_id"
+ case type
+ when "channel.raid"
+ version = "beta"
+ varname = "to_broadcaster_user_id"
+ when "user.update"
+ varname = "user_id"
+ end
+ body = Hash( String, String | Hash( String, String ) ).new
+
+ body = {
+ "type" => type,
+ "version" => version,
+ "condition" => { varname => id.to_s },
+ "transport" => {
+ "method" => "webhook",
+ "callback" => url,
+ "secret" => @settings["eventsub_secret"] # FIXME
+ },
+ }
+ request( "POST", Api::Helix, "/eventsub/subscriptions", body.to_json, contenttype: "application/json" ) end
+ def get_app_token( ); request( "POST", Api::Oauth2, "/token?client_id=#{@settings["client_id"].not_nil!}&client_secret=#{@settings["client_secret"].not_nil!}&grant_type=client_credentials" ) end
+ def get_token_validation( ); request( "GET", Api::Oauth2, "/validate" ) end
# These APIs might require the official Twitch.com web Client-ID now?
def get_panels( login : String ); request( "GET", Api::Api, "/channels/#{login}/panels" ) end
# https://github.com/justintv/Twitch-API/issues/545 requires weird scopes that normal client_ids can't get
diff --git a/util/twitch.cr b/util/twitch.cr
new file mode 100644
index 0000000..1e63736
--- /dev/null
+++ b/util/twitch.cr
@@ -0,0 +1,591 @@
+require "../src/twitcr"
+
+require "http/client"
+require "json"
+require "pretty_print"
+
+settings = Hash(String, String).new
+
+settings["home"] = Path.home.to_s
+["access_token", "channel", "channel_id", "client_id", "client_id_twitch", "client_secret", "app_access_token", "eventsub_secret"].each do |key|
+ begin
+ settings[key] = File.read( settings["home"] + "/.config/twitch/" + key ).chomp
+ rescue
+# STDERR.puts "Warning: Missing " + settings["home"] + "/.config/twitch/" + key
+ end
+end
+
+command = ARGV[0]
+
+client = Twitcr::Client.new( settings )
+
+case command
+when "videos", "lsvideo", "lsvideos"
+ list_videos_kraken( settings, client )
+when "videos_helix" # smaller json, less information
+ list_videos_helix( settings, client )
+when "follows", "lsfollow", "lsfollows"
+ list_user_follows( settings, client )
+when "followees"
+ list_user_followees( settings, client )
+when "followers"
+ list_user_followers( settings, client )
+when "get_follow"
+ pp JSON.parse( client.get_user_follows( from: client.user_id( ARGV[1] ).to_u64, to: client.user_id( ARGV[2] ).to_u64 ) )
+when "follow"
+ pp JSON.parse( client.follow( settings["channel_id"], ARGV[1] ) )
+when "unfollow"
+ pp client.unfollow( settings["channel_id"], ARGV[1] )
+when "streams_followed"
+ list_streams_followed( settings, client )
+when "clips"
+ pp JSON.parse( client.get_clips( ARGV[1] ) )
+when "game"
+ pp client.get_games( ARGV[1] )
+when "streams"
+ list_streams_v5( settings, client, ARGV[1] )
+when "streams_helix"
+ list_streams( settings, JSON.parse( client.get_streams( game_id = client.game_id( ARGV[1] ) ) ) )
+when "hosts"
+ list_hosts( settings, client )
+when "user"
+ pp JSON.parse( client.get_user( ARGV[1] ) )
+when "subs"
+ pp JSON.parse( client.get_subs( client.user( ARGV[1] ).id ) )
+when "eventsubs"
+ pp JSON.parse( client.get_eventsubs( ) )
+when "eventsub"
+ pp JSON.parse( client.put_eventsubs!( ARGV[1], client.user_id( ARGV[2] ).to_u64, ARGV[3] ) )
+when "eventunsub"
+ pp client.delete_eventsubs!( ARGV[1] )
+when "panels"
+ pp JSON.parse( client.get_panels( ARGV[1] ) )
+when "validate"
+ pp JSON.parse( client.get_token_validation() )
+when "app_token"
+ pp JSON.parse( client.get_app_token() )
+when "reset_key"
+ pp JSON.parse( client.delete_key!( settings["channel_id"].to_u64 ) )
+when "update"
+ update_channel( settings, client, settings["channel_id"].to_u64 )
+when "channel"
+ if ARGV.size > 1
+ pp JSON.parse( client.get_channel( client.user_id( ARGV[1] ) ) )
+ else
+ pp JSON.parse( client.get_channel( ) )
+ end
+else
+ puts "ARGV[0] must be one of videos, follows"
+end
+
+# Format cell: widen column as needed
+# FIXME: not Unicode-aware
+macro fcell( data )
+ if {{data}}.bytesize > celw[celi]
+ celw[celi] = {{data}}.bytesize
+ end
+ if celw.sum( foh ) > winsz
+ # don't exceed winsz, even on screwball wide characters
+ if celi == 0
+ # preserve integrity of primary index at expense of last cell
+ celw[-1] = celw[-1] - ( celw.sum( foh ) - winsz )
+ elsif celi != celw.size
+ celw[celw.size]
+ else
+ celw[celi] = celw[celi] - ( celw.sum( foh ) - winsz ) - ( {{data}}.bytesize - {{data}}.size )
+ end
+ end
+ celi+=1
+ { celw[celi-1], celw[celi-1], {{data}} }
+end
+
+macro get_winsz()
+ # Crystal doesn't support the TIOCGWINSZ ioctl
+ `stty size`.split[1].to_i
+end
+
+def list_videos_helix( settings, client )
+
+ # FIXME: handle UInt32 more gracefully
+ # also: implement Twitcr::User so we can restrict valid strings
+ id = client.user( ARGV[1] ).id
+
+ response = JSON.parse( client.get_videos( id ) )
+
+ winsz = get_winsz()
+
+ # cell widths
+ celw=[0,0,0,0,0,0,0]
+ # cell index
+ celi=0
+
+ format = "%*.*s %-*.*s %*.*s %*.*s %*.*s %-*.*s %-*.*s\n"
+
+ # format overhead
+ foh = format.delete( "^ " ).size
+
+ response["data"].as_a.each do |video|
+
+ celi=0
+
+ printf( format,
+ *fcell( video["url"].as_s[12..] ),
+ *fcell( video["created_at"].as_s.[0..15] ),
+ *fcell( video["type"].as_s[0].to_s ),
+ *fcell( video["duration"].as_s[ /([0-9]+[a-z]+)/, 1 ] ),
+ *fcell( video["view_count"].as_i.to_s ),
+ # helix doesn't report game?!
+ # *fcell( video["game"].as_s ),
+ *fcell( video["title"].as_s.chomp ),
+ *fcell( video["description"].as_s.chomp ),
+ )
+
+ end
+
+end
+
+def list_videos_kraken( settings, client )
+
+ # FIXME: handle UInt32 more gracefully
+ # also: implement Twitcr::User so we can restrict valid strings
+ id = client.user( ARGV[1] ).id
+
+ response = JSON.parse( client.get_channels_videos( id ) )
+
+ winsz = get_winsz()
+
+ # cell widths
+ celw=[0,0,0,0,0,0,0,0]
+ # cell index
+ celi=0
+
+ format = "%*.*s %-*.*s %*.*s %*.*s %*.*s %-*.*s %-*.*s %-*.*s\n"
+
+ # format overhead
+ foh = format.delete( "^ " ).size
+
+ response["videos"].as_a.each do |video|
+
+ celi=0
+
+ begin
+ printf( format,
+ *fcell( video["url"].as_s[12..] ),
+ *fcell( video["created_at"].as_s[0..15] ),
+ *fcell( video["broadcast_type"].as_s[0].to_s ),
+ *fcell( video["length"].as_i.to_s ),
+ *fcell( video["views"].as_i.to_s ),
+ *fcell( video["game"].as_s ),
+ *fcell( video["title"].to_s.chomp ),
+ *fcell( video["description"].to_s.chomp ),
+ )
+ rescue ex
+ puts ex.message
+ pp video
+ end
+
+
+ end
+
+end
+
+def list_user_followees( settings, client )
+
+ id = client.user( ARGV[1] ).id
+
+ winsz = get_winsz()
+
+ # cell widths
+ celw=[0,0,0,0,0,0,0,0]
+ # cell index
+ celi=0
+
+ format = "%*.*s %-*.*s %*.*s %*.*s %*.*s %-*.*s %*.*s %-*.*s\n"
+
+ # format overhead
+ foh = format.delete( "^ " ).size
+
+ follows = JSON.parse( client.get_user_follows( from: id ) )["data"]
+
+ followsuids = follows.as_a.map { |follow| follow["to_id"].as_s.to_u64 }
+
+ streams = Hash( UInt64, JSON::Any ).new
+ channels = Hash( UInt64, JSON::Any ).new
+
+ JSON.parse( client.get_streams_v5( channel: followsuids ) )["streams"].as_a.each do |stream|
+ streams[ stream["channel"]["_id"].as_i64.to_u64 ] = stream
+ channels[ stream["channel"]["_id"].as_i64.to_u64 ] = stream["channel"]
+ end
+
+ follows.as_a.each do |follow|
+
+ uid = follow["to_id"].as_s.to_u64
+
+ channel = ( channels[uid]? || JSON.parse( client.get_channel( uid ) ) )
+ stream = ( streams[uid]? || JSON.parse( "{}" ) )
+
+ celi=0
+
+ begin
+ printf( format,
+ #*fcell( follow["followed_at"]?.to_s.[0..9] ),
+ *fcell( follow["followed_at"]?.to_s ),
+ *fcell( follow["to_name"]?.to_s ),
+ *fcell( stream["viewers"]?.to_s ),
+ *fcell( channel["followers"]?.to_s ),
+ *fcell( channel["views"]?.to_s ),
+ *fcell( channel["game"]?.to_s ),
+ *fcell( channel["broadcaster_language"]?.to_s ),
+ *fcell( channel["status"]?.to_s ),
+ )
+ rescue ex
+ puts( ex )
+ pp follow
+ end
+
+ end
+
+end
+
+def list_user_followers( settings, client )
+ id = client.user( ARGV[1] ).id
+
+ winsz = get_winsz()
+
+ # cell widths
+ celw=[0,0,0,0,0,0,0,0]
+ # cell index
+ celi=0
+
+ format = "%*.*s %-*.*s %*.*s %*.*s %*.*s %-*.*s %*.*s %-*.*s\n"
+
+ # format overhead
+ foh = format.delete( "^ " ).size
+
+ puts( "\nLast 100 followers:\n" )
+
+ if ARGV.size > 2
+ followersjson = JSON.parse( client.get_user_follows( to: id, after: ARGV[2] ) )
+ else
+ followersjson = JSON.parse( client.get_user_follows( to: id ) )
+ end
+ followers = followersjson["data"]
+ followersuids = followers.as_a.map { |follow| follow["from_id"].as_s.to_u64 }
+
+ streams = Hash( UInt64, JSON::Any ).new
+ channels = Hash( UInt64, JSON::Any ).new
+
+
+ JSON.parse( client.get_streams_v5( channel: followersuids ) )["streams"].as_a.each do |stream|
+ streams[ stream["channel"]["_id"].as_i64.to_u64 ] = stream
+ channels[ stream["channel"]["_id"].as_i64.to_u64 ] = stream["channel"]
+ end
+
+ followers.as_a.each do |follow|
+
+ uid = follow["from_id"].as_s.to_u64
+
+ channel = ( channels[uid]? || JSON.parse( client.get_channel( uid ) ) )
+ stream = ( streams[uid]? || JSON.parse( "{}" ) )
+
+ celi=0
+
+ begin
+ printf( format,
+ *fcell( follow["followed_at"]?.to_s.[0..15] ),
+ *fcell( follow["from_name"]?.to_s ),
+ *fcell( stream["viewers"]?.to_s ),
+ *fcell( channel["followers"]?.to_s ),
+ *fcell( channel["views"]?.to_s ),
+ *fcell( channel["game"]?.to_s ),
+ *fcell( channel["broadcaster_language"]?.to_s ),
+ *fcell( channel["status"]?.to_s ),
+ )
+ rescue ex
+ puts ex
+ pp follow
+ end
+
+ end
+
+ pp followersjson["pagination"]
+
+end
+
+def list_user_follows( settings, client )
+
+ id = client.user( ARGV[1] ).id
+
+ winsz = get_winsz()
+
+ # cell widths
+ celw=[0,0,0,0,0,0,0,0]
+ # cell index
+ celi=0
+
+ format = "%*.*s %-*.*s %*.*s %*.*s %*.*s %-*.*s %*.*s %-*.*s\n"
+
+ # format overhead
+ foh = format.delete( "^ " ).size
+
+ puts( "Last 100 followees:\n" )
+ follows = JSON.parse( client.get_user_follows( from: id ) )["data"]
+
+ followsuids = follows.as_a.map { |follow| follow["to_id"].as_s.to_u64 }
+
+ streams = Hash( UInt64, JSON::Any ).new
+ channels = Hash( UInt64, JSON::Any ).new
+
+ JSON.parse( client.get_streams_v5( channel: followsuids ) )["streams"].as_a.each do |stream|
+ streams[ stream["channel"]["_id"].as_i64.to_u64 ] = stream
+ channels[ stream["channel"]["_id"].as_i64.to_u64 ] = stream["channel"]
+ end
+
+ follows.as_a.each do |follow|
+
+ uid = follow["to_id"].as_s.to_u64
+
+ channel = ( channels[uid]? || JSON.parse( client.get_channel( uid ) ) )
+ stream = ( streams[uid]? || JSON.parse( "{}" ) )
+
+ celi=0
+
+ begin
+ printf( format,
+ #*fcell( follow["followed_at"]?.to_s.[0..9] ),
+ *fcell( follow["followed_at"]?.to_s ),
+ *fcell( follow["to_name"]?.to_s ),
+ *fcell( stream["viewers"]?.to_s ),
+ *fcell( channel["followers"]?.to_s ),
+ *fcell( channel["views"]?.to_s ),
+ *fcell( channel["game"]?.to_s ),
+ *fcell( channel["broadcaster_language"]?.to_s ),
+ *fcell( channel["status"]?.to_s ),
+ )
+ rescue ex
+ puts( ex )
+ pp follow
+ end
+
+ end
+
+ # cell widths
+ celw=[0,0,0,0,0,0,0,0]
+ # cell index
+ celi=0
+
+ puts( "\nLast 100 followers:\n" )
+ followers = JSON.parse( client.get_user_follows( to: id ) )["data"]
+
+ followersuids = followers.as_a.map { |follow| follow["from_id"].as_s.to_u64 }
+
+ JSON.parse( client.get_streams_v5( channel: followersuids - followsuids ) )["streams"].as_a.each do |stream|
+ streams[ stream["channel"]["_id"].as_i64.to_u64 ] = stream
+ channels[ stream["channel"]["_id"].as_i64.to_u64 ] = stream["channel"]
+ end
+
+ followers.as_a.each do |follow|
+
+ uid = follow["from_id"].as_s.to_u64
+
+ channel = ( channels[uid]? || JSON.parse( client.get_channel( uid ) ) )
+ stream = ( streams[uid]? || JSON.parse( "{}" ) )
+
+ celi=0
+
+ begin
+ printf( format,
+ *fcell( follow["followed_at"]?.to_s.[0..15] ),
+ *fcell( follow["from_name"]?.to_s ),
+ *fcell( stream["viewers"]?.to_s ),
+ *fcell( channel["followers"]?.to_s ),
+ *fcell( channel["views"]?.to_s ),
+ *fcell( channel["game"]?.to_s ),
+ *fcell( channel["broadcaster_language"]?.to_s ),
+ *fcell( channel["status"]?.to_s ),
+ )
+ rescue ex
+ puts ex
+ pp follow
+ end
+
+ end
+
+end
+
+def list_streams_v5( settings, client, game : String )
+
+ response = JSON.parse( client.get_streams_v5( game ) )
+
+ pp response
+
+ winsz = get_winsz()
+
+ # cell widths
+ celw=[0,0,0,0,0,0]
+ # cell index
+ celi=0
+
+ format = "%-*.*s %-*.*s %*.*s %*.*s %*.*s %-*.*s\n"
+
+ # format overhead
+ foh = format.delete( "^ " ).size
+
+ response["streams"].as_a.each do |stream|
+
+ celi=0
+
+ begin
+ printf( format,
+ *fcell( stream["channel"]["url"].as_s[12..] ),
+ *fcell( stream["broadcast_platform"].as_s[0].to_s ),
+ *fcell( stream["viewers"].as_i.to_s ),
+ *fcell( stream["channel"]["followers"].as_i.to_s ),
+ *fcell( stream["game"].as_s ),
+ *fcell( stream["channel"]["status"].to_s.chomp ),
+ )
+ rescue
+ pp stream
+ end
+
+
+ end
+
+end
+
+def list_hosts( settings, client )
+ if ARGV.size > 1
+ channel_id = client.user( ARGV[1] ).id
+ else
+ channel_id = settings["channel_id"].to_u64
+ end
+
+ winsz = get_winsz()
+
+ # cell widths
+ celw=[0,5,6,0,0,0]
+ # cell index
+ celi=0
+
+ format = "%-*.*s %*.*s %*.*s %-*.*s %*.*s %-*.*s\n"
+
+ # format overhead
+ foh = format.delete( "^ " ).size
+
+ json = JSON.parse( client.get_hosts( channel_id ) )
+ json["hosts"].as_a.map { | host |
+ host["host_id"].as_s.to_u64
+ }.each do | uid |
+ pp JSON.parse( client.get_channel( uid ) )
+ end
+ json["hosts"].as_a.map { | host |
+ host["host_id"].as_s.to_u64
+ }.each do | uid |
+
+ channel = ( JSON.parse( client.get_channel( uid ) ) )
+
+ celi=0
+
+ begin
+ printf( format,
+ *fcell( channel["display_name"]?.to_s ),
+ *fcell( channel["followers"]?.to_s ),
+ *fcell( channel["views"]?.to_s ),
+ *fcell( channel["game"]?.to_s ),
+ *fcell( channel["broadcaster_language"]?.to_s ),
+ *fcell( channel["status"]?.to_s ),
+ )
+ rescue ex
+ puts( ex )
+ pp uid
+ pp channel
+ end
+
+ end
+
+# {"hosts" =
+# [{"host_id" => "27345085", "target_id" => "59895482"},
+end
+
+def list_streams( settings, response )
+
+ winsz = get_winsz()
+
+ # cell widths
+ celw=[0,0,0,0,0]
+ # cell index
+ celi=0
+
+ format = "%-*.*s %*.*s %*.*s %-*.*s %-*.*s\n"
+
+ # format overhead
+ foh = format.delete( "^ " ).size
+
+ pp response
+
+ response["data"].as_a.sort{ |a, b| b["viewer_count"].as_i <=> a["viewer_count"].as_i }.each do |stream|
+
+ celi=0
+
+ begin
+ printf( format,
+ *fcell( "twitch.tv/#{stream["user_name"].to_s}" ),
+ *fcell( stream["type"].as_s[0].to_s ),
+ *fcell( stream["viewer_count"].as_i.to_s ),
+ #*fcell( stream["channel"]["followers"].as_i.to_s ),
+ *fcell( stream["game_id"].as_s ),
+ *fcell( stream["title"].to_s ),
+ )
+ rescue
+ pp stream
+ end
+
+ end
+
+end
+
+def list_streams_followed( settings, client )
+
+ winsz = get_winsz()
+
+ # cell widths
+ celw=[0,0,0,0,0,0]
+ # cell index
+ celi=0
+
+ format = "%-*.*s %*.*s %*.*s %*.*s %-*.*s %-*.*s\n"
+
+ # format overhead
+ foh = format.delete( "^ " ).size
+
+ response = JSON.parse( client.get_streams_followed() )
+
+ pp response
+
+ response["streams"].as_a.sort{ |a, b| b["viewers"].as_i <=> a["viewers"].as_i }.each do |stream|
+
+ celi=0
+
+ begin
+ printf( format,
+ *fcell( stream["channel"]["url"].as_s[12..] ),
+ *fcell( stream["stream_type"].as_s[0].to_s ),
+ *fcell( stream["viewers"].as_i.to_s ),
+ *fcell( stream["channel"]["followers"].as_i.to_s ),
+ *fcell( stream["game"].as_s ),
+ *fcell( stream["channel"]["status"].to_s ),
+ )
+ rescue ex
+ pp ex
+ pp stream
+ end
+
+ end
+
+end
+
+def update_channel( settings, client, id )
+
+ pp JSON.parse( client.put_channel!( id, game: ARGV[1], status: ARGV[2] ) )
+
+end