@@ -2,18 +2,21 @@ package youtube
22
33import (
44 "context"
5+ "encoding/json"
56 "errors"
67 "fmt"
78 "math/rand"
89 "net/http"
910 "net/url"
11+ "strings"
1012 "sync"
1113 "time"
1214
1315 ytchat "github.com/abhinavxd/youtube-live-chat-downloader/v2"
1416 "github.com/facebookincubator/go-belt/tool/logger"
1517 "github.com/xaionaro-go/observability"
1618 "github.com/xaionaro-go/streamctl/pkg/streamcontrol"
19+ "github.com/xaionaro-go/xsync"
1720)
1821
1922const youtubeWatchURLString = `https://www.youtube.com/watch`
@@ -50,13 +53,18 @@ func ytWatchURL(videoID string) *url.URL {
5053 return result
5154}
5255
56+ // TODO: delete this handler after explaining to YouTube the application and
57+ // getting a quota for normal ChatListener.
5358type ChatListenerOBSOLETE struct {
5459 videoID string
5560 continuationCode string
5661 clientConfig ytchat.YtCfg
5762 wg sync.WaitGroup
5863 cancelFunc context.CancelFunc
5964 messagesOutChan chan streamcontrol.ChatMessage
65+
66+ channelIDToName map [string ]streamcontrol.ChatUserID
67+ channelIDToNameLocker xsync.Mutex
6068}
6169
6270func NewChatListenerOBSOLETE (
@@ -82,6 +90,7 @@ func NewChatListenerOBSOLETE(
8290 clientConfig : cfg ,
8391 cancelFunc : cancelFunc ,
8492 messagesOutChan : make (chan streamcontrol.ChatMessage , 100 ),
93+ channelIDToName : map [string ]streamcontrol.ChatUserID {},
8594 }
8695 l .wg .Add (1 )
8796 observability .Go (ctx , func (ctx context.Context ) {
@@ -134,7 +143,7 @@ func (l *ChatListenerOBSOLETE) listenLoop(ctx context.Context) (_err error) {
134143 l .messagesOutChan <- streamcontrol.ChatMessage {
135144 CreatedAt : msg .Timestamp ,
136145 EventType : streamcontrol .EventTypeChatMessage ,
137- UserID : streamcontrol . ChatUserID ( msg .AuthorName ),
146+ UserID : l . getUserID ( ctx , msg .AuthorID ),
138147 Username : msg .AuthorName ,
139148 // TODO: find a way to extract the message ID,
140149 // in the mean while we we use a soft key for that:
@@ -145,7 +154,63 @@ func (l *ChatListenerOBSOLETE) listenLoop(ctx context.Context) (_err error) {
145154 }
146155}
147156
148- func (h * ChatListenerOBSOLETE ) Close (ctx context.Context ) error {
157+ func (h * ChatListenerOBSOLETE ) getUserID (
158+ ctx context.Context ,
159+ authorID string ,
160+ ) (_ret streamcontrol.ChatUserID ) {
161+ logger .Tracef (ctx , "getUserID(ctx, '%s')" , authorID )
162+ defer func () { logger .Tracef (ctx , "/getUserID(ctx, '%s'): %v" , authorID , _ret ) }()
163+ return xsync .DoR1 (ctx , & h .channelIDToNameLocker , func () streamcontrol.ChatUserID {
164+ if v , ok := h .channelIDToName [authorID ]; ok {
165+ return v
166+ }
167+
168+ v , err := h .resolveChannelID (ctx , authorID )
169+ if err != nil {
170+ logger .Errorf (ctx , "unable to resolve channel ID '%s': %v" , authorID , err )
171+ return streamcontrol .ChatUserID (authorID )
172+ }
173+
174+ h .channelIDToName [authorID ] = v
175+ return v
176+ })
177+ }
178+
179+ func (h * ChatListenerOBSOLETE ) resolveChannelID (
180+ ctx context.Context ,
181+ authorID string ,
182+ ) (_ret streamcontrol.ChatUserID , _err error ) {
183+ logger .Debugf (ctx , "resolveChannelID(ctx, '%s')" , authorID )
184+ defer func () { logger .Debugf (ctx , "/resolveChannelID(ctx, '%s'): %v %v" , authorID , _ret , _err ) }()
185+
186+ urlString := fmt .Sprintf ("https://www.youtube.com/channel/%s" , authorID )
187+ initialDataBytes , _ , err := ytchat .GetYTDataFromURL (urlString )
188+ if err != nil {
189+ return "" , fmt .Errorf ("unable to get data from '%s': %w" , urlString , err )
190+ }
191+
192+ type initialDataT struct {
193+ Metadata struct {
194+ ChannelMetadataRenderer struct {
195+ VanityChannelURL string `json:"vanityChannelUrl"`
196+ } `json:"channelMetadataRenderer"`
197+ } `json:"metadata"`
198+ }
199+ var initialData initialDataT
200+ if err := json .Unmarshal (initialDataBytes , & initialData ); err != nil {
201+ return "" , fmt .Errorf ("unable to JSON-unmarshal '%s': %w" , initialDataBytes , err )
202+ }
203+
204+ vanityURLString := initialData .Metadata .ChannelMetadataRenderer .VanityChannelURL
205+ vanityURLParts := strings .Split (vanityURLString , "/" )
206+ channelSlug := vanityURLParts [len (vanityURLParts )- 1 ]
207+ channelSlug = strings .Trim (channelSlug , "@" )
208+ return streamcontrol .ChatUserID (channelSlug ), nil
209+ }
210+
211+ func (h * ChatListenerOBSOLETE ) Close (ctx context.Context ) (_err error ) {
212+ logger .Debugf (ctx , "Close(ctx)" )
213+ defer func () { logger .Debugf (ctx , "/Close(ctx): %v" , _err ) }()
149214 h .cancelFunc ()
150215 return nil
151216}
0 commit comments