commit
a114369956
|
@ -0,0 +1,3 @@
|
||||||
|
var/
|
||||||
|
*.db
|
||||||
|
*.csv
|
|
@ -0,0 +1,9 @@
|
||||||
|
module tulpa.dev/cadey/twitchalitics
|
||||||
|
|
||||||
|
go 1.16
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/tailscale/sqlite v0.0.0-20220107190256-d637a57026df // indirect
|
||||||
|
tailscale.com v1.20.1 // indirect
|
||||||
|
within.website/ln v0.9.1 // indirect
|
||||||
|
)
|
|
@ -0,0 +1,111 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"database/sql/driver"
|
||||||
|
"embed"
|
||||||
|
"flag"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/tailscale/sqlite"
|
||||||
|
"tailscale.com/tsnet"
|
||||||
|
"tailscale.com/types/logger"
|
||||||
|
"within.website/ln"
|
||||||
|
"within.website/ln/ex"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
twitchDateFormat = `Mon Jan 02 2006` // Fri Dec 17 2021
|
||||||
|
sqliteDateFormat = "2006-01-02" // 2006-02-01
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
dbLoc = flag.String("db", "var/twitchalitics.db", "path to SQLite database file")
|
||||||
|
hostname = flag.String("hostname", "twitchalitics", "hostname to use on your tailnet")
|
||||||
|
tsLogLoc = flag.String("ts-log-loc", "/mnt/fast/share/twitchalitics.ts.log", "path for Tailscale to dump logs")
|
||||||
|
|
||||||
|
//go:embed schema/*
|
||||||
|
schemaFS embed.FS
|
||||||
|
)
|
||||||
|
|
||||||
|
func getDB() (*sql.DB, error) {
|
||||||
|
connInitFunc := func(ctx context.Context, conn driver.ConnPrepareContext) error {
|
||||||
|
err := sqlite.ExecScript(conn.(sqlite.SQLConn), "PRAGMA journal_mode=WAL;")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
schema, err := schemaFS.ReadFile("schema/schema.sql")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return sqlite.ExecScript(conn.(sqlite.SQLConn), string(schema))
|
||||||
|
}
|
||||||
|
|
||||||
|
db := sql.OpenDB(sqlite.Connector(*dbLoc, connInitFunc, nil))
|
||||||
|
err := db.Ping()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLogFout(fname string) (func(), logger.Logf, error) {
|
||||||
|
os.Remove(fname)
|
||||||
|
fout, err := os.Create(fname)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
done := func() { fout.Close() }
|
||||||
|
lgr := log.New(fout, "", log.LstdFlags)
|
||||||
|
return done, lgr.Printf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
os.Setenv("TAILSCALE_USE_WIP_CODE", "true")
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
db, err := getDB()
|
||||||
|
if err != nil {
|
||||||
|
ln.FatalErr(ctx, err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
done, tsLgr, err := getLogFout(*tsLogLoc)
|
||||||
|
if err != nil {
|
||||||
|
ln.FatalErr(ctx, err)
|
||||||
|
}
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
srv := &tsnet.Server{
|
||||||
|
Hostname: *hostname,
|
||||||
|
Logf: tsLgr,
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = srv
|
||||||
|
|
||||||
|
go plainHTTPRedirect(ctx, srv)
|
||||||
|
ln.FatalErr(ctx, NewServer(db, srv).ListenAndServe())
|
||||||
|
}
|
||||||
|
|
||||||
|
func plainHTTPRedirect(ctx context.Context, srv *tsnet.Server) {
|
||||||
|
l, err := srv.Listen("tcp", ":80")
|
||||||
|
if err != nil {
|
||||||
|
ln.FatalErr(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ln.Log(ctx, ln.Info("listening on :80 to forward to HTTPS"))
|
||||||
|
|
||||||
|
err = http.Serve(l, ex.HTTPLog(http.RedirectHandler("https://twitchalitics.shark-harmonic.ts.net", http.StatusPermanentRedirect)))
|
||||||
|
ln.FatalErr(ctx, err)
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestReadRowsFromFile(t *testing.T) {
|
||||||
|
_, err := ReadRowsFromFile("var/analytics-2021-12-05--2022-01-03.csv")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,110 @@
|
||||||
|
INSERT INTO twitch_revenue
|
||||||
|
( date
|
||||||
|
, ad_break_minutes
|
||||||
|
, ad_time_seconds_per_hour
|
||||||
|
, average_viewers
|
||||||
|
, chat_messages
|
||||||
|
, chatters
|
||||||
|
, clip_views
|
||||||
|
, clips_created
|
||||||
|
, follows
|
||||||
|
, hosts_and_raids_viewers
|
||||||
|
, live_views
|
||||||
|
, max_viewers
|
||||||
|
, minutes_watched
|
||||||
|
, minutes_streamed
|
||||||
|
, unique_viewers
|
||||||
|
, sub_revenue
|
||||||
|
, prime_revenue
|
||||||
|
, gifted_subs_revenue
|
||||||
|
, multi_month_gifted_subs_revenue
|
||||||
|
, bits_revenue
|
||||||
|
, ad_revenue
|
||||||
|
, game_sales_revenue
|
||||||
|
, extensions_revenue
|
||||||
|
, bounties_revenue
|
||||||
|
, prime_subs
|
||||||
|
, total_paid_subs
|
||||||
|
, tier_one_subs
|
||||||
|
, total_gifted_subs
|
||||||
|
, gifted_tier_one_subs
|
||||||
|
, gifted_tier_two_subs
|
||||||
|
, gifted_tier_three_subs
|
||||||
|
, total_multi_month_gifted_subs
|
||||||
|
, multi_month_gifted_tier_one_subs
|
||||||
|
, multi_month_gifted_tier_two_subs
|
||||||
|
, multi_month_gifted_tier_three_subs
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
( ?1
|
||||||
|
, ?2
|
||||||
|
, ?3
|
||||||
|
, ?4
|
||||||
|
, ?5
|
||||||
|
, ?6
|
||||||
|
, ?7
|
||||||
|
, ?8
|
||||||
|
, ?9
|
||||||
|
, ?10
|
||||||
|
, ?11
|
||||||
|
, ?12
|
||||||
|
, ?13
|
||||||
|
, ?14
|
||||||
|
, ?15
|
||||||
|
, ?16
|
||||||
|
, ?17
|
||||||
|
, ?18
|
||||||
|
, ?19
|
||||||
|
, ?20
|
||||||
|
, ?21
|
||||||
|
, ?22
|
||||||
|
, ?23
|
||||||
|
, ?24
|
||||||
|
, ?25
|
||||||
|
, ?26
|
||||||
|
, ?27
|
||||||
|
, ?28
|
||||||
|
, ?29
|
||||||
|
, ?30
|
||||||
|
, ?31
|
||||||
|
, ?32
|
||||||
|
, ?33
|
||||||
|
, ?34
|
||||||
|
, ?35
|
||||||
|
)
|
||||||
|
ON CONFLICT DO
|
||||||
|
UPDATE SET
|
||||||
|
ad_break_minutes = ?2
|
||||||
|
, ad_time_seconds_per_hour = ?3
|
||||||
|
, average_viewers = ?4
|
||||||
|
, chat_messages = ?5
|
||||||
|
, chatters = ?6
|
||||||
|
, clip_views = ?7
|
||||||
|
, clips_created = ?8
|
||||||
|
, follows = ?9
|
||||||
|
, hosts_and_raids_viewers = ?10
|
||||||
|
, live_views = ?11
|
||||||
|
, max_viewers = ?12
|
||||||
|
, minutes_watched = ?13
|
||||||
|
, minutes_streamed = ?14
|
||||||
|
, unique_viewers = ?15
|
||||||
|
, sub_revenue = ?16
|
||||||
|
, prime_revenue = ?17
|
||||||
|
, gifted_subs_revenue = ?18
|
||||||
|
, multi_month_gifted_subs_revenue = ?19
|
||||||
|
, bits_revenue = ?20
|
||||||
|
, ad_revenue = ?21
|
||||||
|
, game_sales_revenue = ?22
|
||||||
|
, extensions_revenue = ?23
|
||||||
|
, bounties_revenue = ?24
|
||||||
|
, prime_subs = ?25
|
||||||
|
, total_paid_subs = ?26
|
||||||
|
, tier_one_subs = ?27
|
||||||
|
, total_gifted_subs = ?28
|
||||||
|
, gifted_tier_one_subs = ?29
|
||||||
|
, gifted_tier_two_subs = ?30
|
||||||
|
, gifted_tier_three_subs = ?31
|
||||||
|
, total_multi_month_gifted_subs = ?32
|
||||||
|
, multi_month_gifted_tier_one_subs = ?33
|
||||||
|
, multi_month_gifted_tier_two_subs = ?34
|
||||||
|
, multi_month_gifted_tier_three_subs = ?35
|
|
@ -0,0 +1,39 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS twitch_revenue
|
||||||
|
( date TEXT PRIMARY KEY
|
||||||
|
, ad_break_minutes INTEGER NOT NULL
|
||||||
|
, ad_time_seconds_per_hour REAL NOT NULL
|
||||||
|
, average_viewers REAL NOT NULL
|
||||||
|
, chat_messages INTEGER NOT NULL
|
||||||
|
, chatters INTEGER NOT NULL
|
||||||
|
, clip_views INTEGER NOT NULL
|
||||||
|
, clips_created INTEGER NOT NULL
|
||||||
|
, follows INTEGER NOT NULL
|
||||||
|
, hosts_and_raids_viewers INTEGER NOT NULL
|
||||||
|
, live_views INTEGER NOT NULL
|
||||||
|
, max_viewers INTEGER NOT NULL
|
||||||
|
, minutes_watched INTEGER NOT NULL
|
||||||
|
, minutes_streamed INTEGER NOT NULL
|
||||||
|
, unique_viewers INTEGER NOT NULL
|
||||||
|
, sub_revenue REAL NOT NULL
|
||||||
|
, prime_revenue REAL NOT NULL
|
||||||
|
, gifted_subs_revenue REAL NOT NULL
|
||||||
|
, multi_month_gifted_subs_revenue REAL NOT NULL
|
||||||
|
, bits_revenue REAL NOT NULL
|
||||||
|
, ad_revenue REAL NOT NULL
|
||||||
|
, game_sales_revenue REAL NOT NULL
|
||||||
|
, extensions_revenue REAL NOT NULL
|
||||||
|
, bounties_revenue REAL NOT NULL
|
||||||
|
, prime_subs INTEGER NOT NULL
|
||||||
|
, total_paid_subs INTEGER NOT NULL
|
||||||
|
, tier_one_subs INTEGER NOT NULL
|
||||||
|
, total_gifted_subs INTEGER NOT NULL
|
||||||
|
, gifted_tier_one_subs INTEGER NOT NULL
|
||||||
|
, gifted_tier_two_subs INTEGER NOT NULL
|
||||||
|
, gifted_tier_three_subs INTEGER NOT NULL
|
||||||
|
, total_multi_month_gifted_subs INTEGER NOT NULL
|
||||||
|
, multi_month_gifted_tier_one_subs INTEGER NOT NULL
|
||||||
|
, multi_month_gifted_tier_two_subs INTEGER NOT NULL
|
||||||
|
, multi_month_gifted_tier_three_subs INTEGER NOT NULL
|
||||||
|
-- generated columns
|
||||||
|
, total_revenue REAL GENERATED ALWAYS AS (sub_revenue + prime_revenue + gifted_subs_revenue + multi_month_gifted_subs_revenue + bits_revenue + ad_revenue + game_sales_revenue + extensions_revenue + bounties_revenue)
|
||||||
|
);
|
|
@ -0,0 +1,87 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"tailscale.com/client/tailscale"
|
||||||
|
"tailscale.com/tsnet"
|
||||||
|
"within.website/ln"
|
||||||
|
"within.website/ln/ex"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
db *sql.DB
|
||||||
|
mux *http.ServeMux
|
||||||
|
srv *tsnet.Server
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServer(db *sql.DB, srv *tsnet.Server) *Server {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
|
s := &Server{
|
||||||
|
db: db,
|
||||||
|
mux: mux,
|
||||||
|
srv: srv,
|
||||||
|
}
|
||||||
|
|
||||||
|
mux.HandleFunc("/api/whois", s.whois)
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) ListenAndServe() error {
|
||||||
|
l, err := s.srv.Listen("tcp", ":443")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
l = tls.NewListener(l, &tls.Config{
|
||||||
|
GetCertificate: func(chi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
|
c, err := tailscale.GetCertificate(chi)
|
||||||
|
if err != nil {
|
||||||
|
ln.Error(context.Background(), err, ln.F{"remote_addr": chi.Conn.RemoteAddr().String()})
|
||||||
|
}
|
||||||
|
return c, err
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
hs := &http.Server{
|
||||||
|
IdleTimeout: 5 * time.Minute,
|
||||||
|
Handler: ex.HTTPLog(s),
|
||||||
|
}
|
||||||
|
|
||||||
|
ln.Log(context.Background(), ln.Info("listening on https://twitchalitics.shark-harmonic.ts.net"))
|
||||||
|
|
||||||
|
return hs.Serve(l)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Server) whois(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userInfo, err := tailscale.WhoIs(r.Context(), r.RemoteAddr)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "can't get whois response: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
ln.Error(r.Context(), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(userInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userInfo, err := tailscale.WhoIs(r.Context(), r.RemoteAddr)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "can't get whois response: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
ln.Error(r.Context(), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := ln.WithF(r.Context(), ln.F{"username": userInfo.UserProfile.LoginName, "hostname": userInfo.Node.ComputedName, "os": userInfo.Node.Hostinfo.OS})
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
|
||||||
|
s.mux.ServeHTTP(w, r)
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
{ pkgs ?
|
||||||
|
import <nixpkgs> { overlays = [ (self: super: { go = super.go_1_17; }) ]; } }:
|
||||||
|
|
||||||
|
pkgs.mkShell {
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
go
|
||||||
|
gopls
|
||||||
|
goimports
|
||||||
|
sqlite-interactive
|
||||||
|
pkg-config
|
||||||
|
|
||||||
|
# keep this line if you use bash
|
||||||
|
pkgs.bashInteractive
|
||||||
|
];
|
||||||
|
}
|
|
@ -0,0 +1,354 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/csv"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ImportCSV(ctx context.Context, db *sql.DB, dir string) error {
|
||||||
|
files, err := filepath.Glob(filepath.Join(dir, "*.csv"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, fname := range files {
|
||||||
|
rows, err := ReadRowsFromFile(fname)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, row := range rows {
|
||||||
|
err := row.Upsert(tx)
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.Commit()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type TwitchRevenueCSV struct {
|
||||||
|
Date time.Time
|
||||||
|
AdBreakMinutes int
|
||||||
|
AdTimeSecondsPerHour float64
|
||||||
|
AverageViewers float64
|
||||||
|
ChatMessages int
|
||||||
|
Chatters int
|
||||||
|
ClipViews int
|
||||||
|
ClipsCreated int
|
||||||
|
Follows int
|
||||||
|
HostsAndRaidsViewers int
|
||||||
|
LiveViews int
|
||||||
|
MaxViewers int
|
||||||
|
MinutesWatched int
|
||||||
|
MinutesStreamed int
|
||||||
|
UniqueViewers int
|
||||||
|
SubRevenue float64
|
||||||
|
PrimeRevenue float64
|
||||||
|
GiftedSubsRevenue float64
|
||||||
|
MultiMonthGiftedSubsRevenue float64
|
||||||
|
BitsRevenue float64
|
||||||
|
AdRevenue float64
|
||||||
|
GameSalesRevenue float64
|
||||||
|
ExtensionsRevenue float64
|
||||||
|
BountiesRevenue float64
|
||||||
|
PrimeSubs int
|
||||||
|
TotalPaidSubs int
|
||||||
|
TierOneSubs int
|
||||||
|
TotalGiftedSubs int
|
||||||
|
GiftedTierOneSubs int
|
||||||
|
GiftedTierTwoSubs int
|
||||||
|
GiftedTierThreeSubs int
|
||||||
|
TotalMultiMonthGiftedSubs int
|
||||||
|
MultiMonthGiftedTierOneSubs int
|
||||||
|
MultiMonthGiftedTierTwoSubs int
|
||||||
|
MultiMonthGiftedTierThreeSubs int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TwitchRevenueCSV) Upsert(tx *sql.Tx) error {
|
||||||
|
qBytes, err := schemaFS.ReadFile("schema/insert.sql")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = tx.Exec(
|
||||||
|
string(qBytes),
|
||||||
|
t.Date.Format(sqliteDateFormat),
|
||||||
|
t.AdBreakMinutes,
|
||||||
|
t.AdTimeSecondsPerHour,
|
||||||
|
t.AverageViewers,
|
||||||
|
t.ChatMessages,
|
||||||
|
t.Chatters,
|
||||||
|
t.ClipViews,
|
||||||
|
t.ClipsCreated,
|
||||||
|
t.Follows,
|
||||||
|
t.HostsAndRaidsViewers,
|
||||||
|
t.LiveViews,
|
||||||
|
t.MaxViewers,
|
||||||
|
t.MinutesWatched,
|
||||||
|
t.MinutesStreamed,
|
||||||
|
t.UniqueViewers,
|
||||||
|
t.SubRevenue,
|
||||||
|
t.PrimeRevenue,
|
||||||
|
t.GiftedSubsRevenue,
|
||||||
|
t.MultiMonthGiftedSubsRevenue,
|
||||||
|
t.BitsRevenue,
|
||||||
|
t.AdRevenue,
|
||||||
|
t.GameSalesRevenue,
|
||||||
|
t.ExtensionsRevenue,
|
||||||
|
t.BountiesRevenue,
|
||||||
|
t.PrimeSubs,
|
||||||
|
t.TotalPaidSubs,
|
||||||
|
t.TierOneSubs,
|
||||||
|
t.TotalGiftedSubs,
|
||||||
|
t.GiftedTierOneSubs,
|
||||||
|
t.GiftedTierTwoSubs,
|
||||||
|
t.GiftedTierThreeSubs,
|
||||||
|
t.TotalMultiMonthGiftedSubs,
|
||||||
|
t.MultiMonthGiftedTierOneSubs,
|
||||||
|
t.MultiMonthGiftedTierTwoSubs,
|
||||||
|
t.MultiMonthGiftedTierThreeSubs,
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadRowsFromFile(fname string) ([]*TwitchRevenueCSV, error) {
|
||||||
|
var result []*TwitchRevenueCSV
|
||||||
|
|
||||||
|
fin, err := os.Open(fname)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't open file: %w", err)
|
||||||
|
}
|
||||||
|
defer fin.Close()
|
||||||
|
|
||||||
|
rdr := csv.NewReader(fin)
|
||||||
|
|
||||||
|
_, err = rdr.Read() // discard header row
|
||||||
|
var n = 1
|
||||||
|
|
||||||
|
for {
|
||||||
|
row, err := rdr.Read()
|
||||||
|
if err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("can't read csv row: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
val, err := ReadFromRow(row)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't parse row %d %w", n, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadFromRow(inp []string) (*TwitchRevenueCSV, error) {
|
||||||
|
var (
|
||||||
|
result TwitchRevenueCSV
|
||||||
|
err error
|
||||||
|
n int
|
||||||
|
)
|
||||||
|
|
||||||
|
result.Date, err = time.Parse(twitchDateFormat, inp[n])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
result.AdBreakMinutes, err = strconv.Atoi(inp[n])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
result.AdTimeSecondsPerHour, err = strconv.ParseFloat(inp[n], 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
result.AverageViewers, err = strconv.ParseFloat(inp[n], 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
result.ChatMessages, err = strconv.Atoi(inp[n])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
result.Chatters, err = strconv.Atoi(inp[n])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
result.ClipViews, err = strconv.Atoi(inp[n])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
result.ClipsCreated, err = strconv.Atoi(inp[n])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
result.Follows, err = strconv.Atoi(inp[n])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
result.HostsAndRaidsViewers, err = strconv.Atoi(inp[n])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
result.LiveViews, err = strconv.Atoi(inp[n])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
result.MaxViewers, err = strconv.Atoi(inp[n])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
result.MinutesWatched, err = strconv.Atoi(inp[n])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
result.MinutesStreamed, err = strconv.Atoi(inp[n])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
result.UniqueViewers, err = strconv.Atoi(inp[n])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
result.SubRevenue, err = strconv.ParseFloat(inp[n], 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
result.PrimeRevenue, err = strconv.ParseFloat(inp[n], 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
result.GiftedSubsRevenue, err = strconv.ParseFloat(inp[n], 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
result.MultiMonthGiftedSubsRevenue, err = strconv.ParseFloat(inp[n], 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
result.BitsRevenue, err = strconv.ParseFloat(inp[n], 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
result.AdRevenue, err = strconv.ParseFloat(inp[n], 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
result.GameSalesRevenue, err = strconv.ParseFloat(inp[n], 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
result.ExtensionsRevenue, err = strconv.ParseFloat(inp[n], 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
result.BountiesRevenue, err = strconv.ParseFloat(inp[n], 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
result.PrimeSubs, err = strconv.Atoi(inp[n])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
result.TotalPaidSubs, err = strconv.Atoi(inp[n])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
result.TierOneSubs, err = strconv.Atoi(inp[n])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
result.TotalGiftedSubs, err = strconv.Atoi(inp[n])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
result.GiftedTierOneSubs, err = strconv.Atoi(inp[n])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
result.GiftedTierTwoSubs, err = strconv.Atoi(inp[n])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
result.GiftedTierThreeSubs, err = strconv.Atoi(inp[n])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
result.TotalMultiMonthGiftedSubs, err = strconv.Atoi(inp[n])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
result.MultiMonthGiftedTierOneSubs, err = strconv.Atoi(inp[n])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
result.MultiMonthGiftedTierTwoSubs, err = strconv.Atoi(inp[n])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
result.MultiMonthGiftedTierThreeSubs, err = strconv.Atoi(inp[n])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
|
||||||
|
return &result, err
|
||||||
|
}
|
Loading…
Reference in New Issue