diff options
author | Joe Rayhawk <jrayhawk@action.fairlystable.org> | 2021-10-15 21:39:13 -0700 |
---|---|---|
committer | Joe Rayhawk <jrayhawk@action.fairlystable.org> | 2021-10-16 00:11:13 -0700 |
commit | 668e9ca91ba3bd2460a221db366941ac456ea990 (patch) | |
tree | 53708727a4a14a79d8eba4d066bb53070053238a | |
parent | 0ce7fd4bf61d861a96d20eee65aa07615929b9cb (diff) | |
download | twitcr-668e9ca91ba3bd2460a221db366941ac456ea990.tar.gz twitcr-668e9ca91ba3bd2460a221db366941ac456ea990.zip |
various improvements
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | README.md | 2 | ||||
-rw-r--r-- | src/twitcr/mappings/game.cr | 21 | ||||
-rw-r--r-- | src/twitcr/rest.cr | 58 | ||||
-rw-r--r-- | util/twitch.cr | 591 |
5 files changed, 669 insertions, 4 deletions
@@ -7,3 +7,4 @@ # Libraries don't need dependency lock # Dependencies will be locked in applications that use them /shard.lock +/util/twitch @@ -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 |