-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
257 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
= Why is your stream so quiet? | ||
|
||
I learned the hard way that muting everything is best when just sharing a terminal in over-the-shoulder mode. For more on that and other lessons, see my perpetually unpublished link:https://rwxrob.github.io/books/bad-strimmer/[_Bad Strimmer_]. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
module github.com/rwxrob/books/adoc/terminal-velocity/golang/yt2wee | ||
|
||
go 1.23.4 | ||
|
||
require google.golang.org/api v0.213.0 | ||
|
||
require ( | ||
cloud.google.com/go/auth v0.13.0 // indirect | ||
cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect | ||
cloud.google.com/go/compute/metadata v0.6.0 // indirect | ||
github.com/felixge/httpsnoop v1.0.4 // indirect | ||
github.com/go-logr/logr v1.4.2 // indirect | ||
github.com/go-logr/stdr v1.2.2 // indirect | ||
github.com/google/s2a-go v0.1.8 // indirect | ||
github.com/google/uuid v1.6.0 // indirect | ||
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect | ||
github.com/googleapis/gax-go/v2 v2.14.0 // indirect | ||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect | ||
go.opentelemetry.io/otel v1.29.0 // indirect | ||
go.opentelemetry.io/otel/metric v1.29.0 // indirect | ||
go.opentelemetry.io/otel/trace v1.29.0 // indirect | ||
golang.org/x/crypto v0.31.0 // indirect | ||
golang.org/x/net v0.32.0 // indirect | ||
golang.org/x/oauth2 v0.24.0 // indirect | ||
golang.org/x/sys v0.28.0 // indirect | ||
golang.org/x/text v0.21.0 // indirect | ||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect | ||
google.golang.org/grpc v1.67.1 // indirect | ||
google.golang.org/protobuf v1.35.2 // indirect | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
cloud.google.com/go/auth v0.13.0 h1:8Fu8TZy167JkW8Tj3q7dIkr2v4cndv41ouecJx0PAHs= | ||
cloud.google.com/go/auth v0.13.0/go.mod h1:COOjD9gwfKNKz+IIduatIhYJQIc0mG3H102r/EMxX6Q= | ||
cloud.google.com/go/auth/oauth2adapt v0.2.6 h1:V6a6XDu2lTwPZWOawrAa9HUK+DB2zfJyTuciBG5hFkU= | ||
cloud.google.com/go/auth/oauth2adapt v0.2.6/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8= | ||
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= | ||
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= | ||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= | ||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= | ||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= | ||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= | ||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= | ||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= | ||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= | ||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= | ||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= | ||
github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= | ||
github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= | ||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= | ||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||
github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= | ||
github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= | ||
github.com/googleapis/gax-go/v2 v2.14.0 h1:f+jMrjBPl+DL9nI4IQzLUxMq7XrAqFYB7hBPqMNIe8o= | ||
github.com/googleapis/gax-go/v2 v2.14.0/go.mod h1:lhBCnjdLrWRaPvLWhmc8IS24m9mr07qSYnHncrgo+zk= | ||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= | ||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= | ||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= | ||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= | ||
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= | ||
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= | ||
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= | ||
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= | ||
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= | ||
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= | ||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= | ||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= | ||
golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= | ||
golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= | ||
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= | ||
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= | ||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= | ||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= | ||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= | ||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= | ||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= | ||
google.golang.org/api v0.213.0 h1:KmF6KaDyFqB417T68tMPbVmmwtIXs2VB60OJKIHB0xQ= | ||
google.golang.org/api v0.213.0/go.mod h1:V0T5ZhNUUNpYAlL306gFZPFt5F5D/IeyLoktduYYnvQ= | ||
google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 h1:M0KvPgPmDZHPlbRbaNU1APr28TvwvvdUPlSv7PUvy8g= | ||
google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:dguCy7UOdZhTvLzDyt15+rOrawrpM4q7DD9dQ1P11P4= | ||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY= | ||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= | ||
google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= | ||
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= | ||
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= | ||
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= | ||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"log" | ||
"sync" | ||
"time" | ||
|
||
"google.golang.org/api/option" | ||
"google.golang.org/api/youtube/v3" | ||
) | ||
|
||
const ( | ||
apiKey = "YOUR_YOUTUBE_API_KEY" // Replace with your API key | ||
channelID = "YOUR_CHANNEL_ID" // Replace with your channel ID | ||
pollInterval = 5 * time.Second // Adjust as needed | ||
expiryTime = 10 * time.Minute // Time after which entries expire | ||
cleanupInterval = 1 * time.Minute // Interval to run cleanup | ||
) | ||
|
||
// ChatMessage represents a chat message from the source | ||
type ChatMessage struct { | ||
User string | ||
Message string | ||
} | ||
|
||
// Global map with timestamps and a mutex for thread safety | ||
var ( | ||
seenMessages = make(map[string]time.Time) | ||
mu sync.Mutex | ||
) | ||
|
||
// fetchChatMessages fetches messages from YouTube Live Chat and deduplicates them | ||
func fetchChatMessages(service *youtube.Service, liveChatID string, nextPageToken string) (messages []ChatMessage, newNextPageToken string, err error) { | ||
call := service.LiveChatMessages.List(liveChatID, []string{"snippet", "authorDetails"}).Key(apiKey) | ||
if nextPageToken != "" { | ||
call = call.PageToken(nextPageToken) | ||
} | ||
|
||
response, err := call.Do() | ||
if err != nil { | ||
return nil, "", fmt.Errorf("error fetching live chat messages: %w", err) | ||
} | ||
|
||
mu.Lock() | ||
defer mu.Unlock() | ||
|
||
now := time.Now() | ||
for _, item := range response.Items { | ||
if _, exists := seenMessages[item.Id]; !exists { | ||
seenMessages[item.Id] = now // Mark the message as seen with the current timestamp | ||
messages = append(messages, ChatMessage{ | ||
User: item.AuthorDetails.DisplayName, | ||
Message: item.Snippet.DisplayMessage, | ||
}) | ||
} | ||
} | ||
|
||
return messages, response.NextPageToken, nil | ||
} | ||
|
||
// cleanupSeenMessages removes old entries from the seenMessages map | ||
func cleanupSeenMessages() { | ||
for { | ||
time.Sleep(cleanupInterval) | ||
|
||
mu.Lock() | ||
now := time.Now() | ||
for id, timestamp := range seenMessages { | ||
if now.Sub(timestamp) > expiryTime { | ||
delete(seenMessages, id) | ||
} | ||
} | ||
mu.Unlock() | ||
} | ||
} | ||
|
||
// getLiveChatID fetches the live chat ID for the current live stream | ||
func getLiveChatID(service *youtube.Service) (string, error) { | ||
call := service.Search.List([]string{"snippet"}). | ||
ChannelId(channelID). | ||
EventType("live"). | ||
Type("video"). | ||
Key(apiKey) | ||
|
||
response, err := call.Do() | ||
if err != nil { | ||
return "", fmt.Errorf("error fetching live broadcasts: %w", err) | ||
} | ||
|
||
if len(response.Items) == 0 { | ||
return "", fmt.Errorf("no live broadcasts found for channel ID %s", channelID) | ||
} | ||
|
||
videoID := response.Items[0].Id.VideoId | ||
|
||
videoCall := service.Videos.List([]string{"liveStreamingDetails"}). | ||
Id(videoID). | ||
Key(apiKey) | ||
|
||
videoResponse, err := videoCall.Do() | ||
if err != nil { | ||
return "", fmt.Errorf("error fetching video details: %w", err) | ||
} | ||
|
||
if len(videoResponse.Items) == 0 { | ||
return "", fmt.Errorf("no video details found for video ID %s", videoID) | ||
} | ||
|
||
liveChatID := videoResponse.Items[0].LiveStreamingDetails.ActiveLiveChatId | ||
if liveChatID == "" { | ||
return "", fmt.Errorf("live chat ID not found for video ID %s", videoID) | ||
} | ||
|
||
return liveChatID, nil | ||
} | ||
|
||
func main() { | ||
ctx := context.Background() | ||
service, err := youtube.NewService(ctx, option.WithAPIKey(apiKey)) | ||
if err != nil { | ||
log.Fatalf("Error creating YouTube service: %v", err) | ||
} | ||
|
||
liveChatID, err := getLiveChatID(service) | ||
if err != nil { | ||
log.Fatalf("Error getting live chat ID: %v", err) | ||
} | ||
|
||
// Start the cleanup routine | ||
go cleanupSeenMessages() | ||
|
||
var nextPageToken string | ||
for { | ||
messages, newNextPageToken, err := fetchChatMessages(service, liveChatID, nextPageToken) | ||
if err != nil { | ||
log.Printf("Error fetching chat messages: %v", err) | ||
time.Sleep(pollInterval) | ||
continue | ||
} | ||
|
||
for _, msg := range messages { | ||
fmt.Printf("%s: %s\n", msg.User, msg.Message) | ||
// Here, you would send the message to WeeChat FIFO | ||
} | ||
|
||
nextPageToken = newNextPageToken | ||
time.Sleep(pollInterval) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters