analyzer.go 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. package analysis
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "time"
  7. "git.linuxforward.com/byom/byom-trends/common"
  8. "git.linuxforward.com/byom/byom-trends/logger"
  9. "git.linuxforward.com/byom/byom-trends/services/google"
  10. "git.linuxforward.com/byom/byom-trends/services/instagram"
  11. "git.linuxforward.com/byom/byom-trends/services/tiktok"
  12. "git.linuxforward.com/byom/byom-trends/services/youtube"
  13. "github.com/sirupsen/logrus"
  14. )
  15. type TrendAnalyzer struct {
  16. instagramSvc *instagram.Client
  17. tiktokSvc *tiktok.Client
  18. youtubeSvc *youtube.Client
  19. googleSvc *google.Client
  20. logger *logrus.Entry
  21. }
  22. type EngagementMetrics struct {
  23. Platform string `json:"platform"`
  24. Engagement float64 `json:"engagement"`
  25. PostCount int64 `json:"post_count"`
  26. Followers int64 `json:"followers"`
  27. TotalLikes int64 `json:"total_likes"`
  28. TotalViews int64 `json:"total_views"`
  29. GrowthRate float64 `json:"growth_rate"`
  30. }
  31. type ContentTrends struct {
  32. TopHashtags map[string]int `json:"top_hashtags"`
  33. TopKeywords map[string]int `json:"top_keywords"`
  34. PopularTopics []string `json:"popular_topics"`
  35. BestPerforming map[string]string `json:"best_performing"` // platform -> content ID
  36. PostingTimes map[string]int `json:"posting_times"` // hour -> count
  37. }
  38. type AudienceMetrics struct {
  39. Demographics map[string]float64 `json:"demographics"`
  40. Interests []string `json:"interests"`
  41. Locations map[string]int `json:"locations"`
  42. ActiveHours map[int]int `json:"active_hours"`
  43. }
  44. func NewTrendAnalyzer(
  45. instagramSvc *instagram.Client,
  46. tiktokSvc *tiktok.Client,
  47. youtubeSvc *youtube.Client,
  48. googleSvc *google.Client,
  49. ) *TrendAnalyzer {
  50. return &TrendAnalyzer{
  51. instagramSvc: instagramSvc,
  52. tiktokSvc: tiktokSvc,
  53. youtubeSvc: youtubeSvc,
  54. googleSvc: googleSvc,
  55. logger: logger.NewLogger("trend-analyzer"),
  56. }
  57. }
  58. func (a *TrendAnalyzer) AnalyzeEngagement(ctx context.Context, profile *common.SocialProfile) (*EngagementMetrics, error) {
  59. metrics := &EngagementMetrics{
  60. Platform: profile.Platform,
  61. }
  62. switch profile.Platform {
  63. case "instagram":
  64. stats, err := a.instagramSvc.GetProfileStats(profile.Username)
  65. if err != nil {
  66. return nil, fmt.Errorf("get instagram stats: %w", err)
  67. }
  68. posts, err := a.instagramSvc.GetRecentPosts(profile.Username, 50)
  69. if err != nil {
  70. return nil, fmt.Errorf("get instagram posts: %w", err)
  71. }
  72. metrics.Engagement = a.instagramSvc.CalculateEngagement(posts)
  73. metrics.Followers = stats.FollowerCount
  74. case "tiktok":
  75. stats, err := a.tiktokSvc.GetProfileStats(profile.Username)
  76. if err != nil {
  77. return nil, fmt.Errorf("get tiktok stats: %w", err)
  78. }
  79. videos, err := a.tiktokSvc.GetRecentVideos(profile.Username, 50)
  80. if err != nil {
  81. return nil, fmt.Errorf("get tiktok videos: %w", err)
  82. }
  83. metrics.Engagement = a.tiktokSvc.CalculateEngagement(videos)
  84. metrics.Followers = stats.FollowerCount
  85. case "youtube":
  86. stats, err := a.youtubeSvc.GetChannelStats(profile.Username)
  87. if err != nil {
  88. return nil, fmt.Errorf("get youtube stats: %w", err)
  89. }
  90. videos, err := a.youtubeSvc.GetRecentVideos(profile.Username, 50)
  91. if err != nil {
  92. return nil, fmt.Errorf("get youtube videos: %w", err)
  93. }
  94. metrics.Engagement = a.youtubeSvc.CalculateEngagement(videos)
  95. metrics.Followers = stats.SubscriberCount
  96. }
  97. return metrics, nil
  98. }
  99. func (a *TrendAnalyzer) AnalyzeContent(ctx context.Context, profile *common.SocialProfile) (*ContentTrends, error) {
  100. trends := &ContentTrends{
  101. TopHashtags: make(map[string]int),
  102. TopKeywords: make(map[string]int),
  103. PostingTimes: make(map[string]int),
  104. BestPerforming: make(map[string]string),
  105. }
  106. switch profile.Platform {
  107. case "instagram":
  108. posts, err := a.instagramSvc.GetRecentPosts(profile.Username, 100)
  109. if err != nil {
  110. return nil, fmt.Errorf("get instagram posts: %w", err)
  111. }
  112. // Analyze posting times
  113. for _, post := range posts {
  114. hour := post.Timestamp.Format("15")
  115. trends.PostingTimes[hour]++
  116. }
  117. case "tiktok":
  118. videos, err := a.tiktokSvc.GetRecentVideos(profile.Username, 100)
  119. if err != nil {
  120. return nil, fmt.Errorf("get tiktok videos: %w", err)
  121. }
  122. trends.TopHashtags = a.tiktokSvc.GetTrendingHashtags(videos)
  123. case "youtube":
  124. videos, err := a.youtubeSvc.GetRecentVideos(profile.Username, 100)
  125. if err != nil {
  126. return nil, fmt.Errorf("get youtube videos: %w", err)
  127. }
  128. trends.TopHashtags = a.youtubeSvc.GetTrendingTags(videos)
  129. }
  130. // Get related topics from Google Trends
  131. topics, err := a.googleSvc.GetRelatedTopics(profile.Username)
  132. if err != nil {
  133. a.logger.WithError(err).Warn("failed to get related topics")
  134. } else {
  135. for _, topic := range topics {
  136. trends.PopularTopics = append(trends.PopularTopics, topic.Title)
  137. }
  138. }
  139. return trends, nil
  140. }
  141. func (a *TrendAnalyzer) AnalyzeAudience(ctx context.Context, profile *common.SocialProfile) (*AudienceMetrics, error) {
  142. metrics := &AudienceMetrics{
  143. Demographics: make(map[string]float64),
  144. Locations: make(map[string]int),
  145. ActiveHours: make(map[int]int),
  146. }
  147. // Get regional interest from Google Trends
  148. locations, err := a.googleSvc.GetRegionalInterest(profile.Username, "")
  149. if err != nil {
  150. a.logger.WithError(err).Warn("failed to get regional interest")
  151. } else {
  152. metrics.Locations = locations
  153. }
  154. // Analyze active hours based on content posting times and engagement
  155. switch profile.Platform {
  156. case "instagram":
  157. posts, err := a.instagramSvc.GetRecentPosts(profile.Username, 100)
  158. if err != nil {
  159. return nil, fmt.Errorf("get instagram posts: %w", err)
  160. }
  161. for _, post := range posts {
  162. hour := post.Timestamp.Hour()
  163. metrics.ActiveHours[hour]++
  164. }
  165. case "tiktok":
  166. videos, err := a.tiktokSvc.GetRecentVideos(profile.Username, 100)
  167. if err != nil {
  168. return nil, fmt.Errorf("get tiktok videos: %w", err)
  169. }
  170. for _, video := range videos {
  171. hour := video.CreateTime.Hour()
  172. metrics.ActiveHours[hour]++
  173. }
  174. case "youtube":
  175. videos, err := a.youtubeSvc.GetRecentVideos(profile.Username, 100)
  176. if err != nil {
  177. return nil, fmt.Errorf("get youtube videos: %w", err)
  178. }
  179. for _, video := range videos {
  180. hour := video.PublishedAt.Hour()
  181. metrics.ActiveHours[hour]++
  182. }
  183. }
  184. return metrics, nil
  185. }
  186. func (a *TrendAnalyzer) GenerateReport(ctx context.Context, profile *common.SocialProfile) (*common.TrendAnalysis, error) {
  187. engagement, err := a.AnalyzeEngagement(ctx, profile)
  188. if err != nil {
  189. return nil, fmt.Errorf("analyze engagement: %w", err)
  190. }
  191. content, err := a.AnalyzeContent(ctx, profile)
  192. if err != nil {
  193. return nil, fmt.Errorf("analyze content: %w", err)
  194. }
  195. audience, err := a.AnalyzeAudience(ctx, profile)
  196. if err != nil {
  197. return nil, fmt.Errorf("analyze audience: %w", err)
  198. }
  199. // Combine all analyses into a single report
  200. report := map[string]interface{}{
  201. "engagement": engagement,
  202. "content": content,
  203. "audience": audience,
  204. "timestamp": time.Now(),
  205. }
  206. reportJSON, err := json.Marshal(report)
  207. if err != nil {
  208. return nil, fmt.Errorf("marshal report: %w", err)
  209. }
  210. analysis := &common.TrendAnalysis{
  211. Type: "comprehensive",
  212. Data: reportJSON,
  213. }
  214. return analysis, nil
  215. }