Analytics for Asia-Pacific Video Trends
Understanding which regions drive traffic, which categories trend at which hours, and how K-pop compares to J-drama in engagement — these are the questions that shape content strategy for TopVideoHub. A lightweight Go backend serving pre-aggregated analytics to Chart.js covers all of it.
Go Analytics Server
package main
import (
"database/sql"
"encoding/json"
"log"
"net/http"
_ "github.com/mattn/go-sqlite3"
)
type Server struct {
db *sql.DB
}
func main() {
db, err := sql.Open("sqlite3", "./data/topvideohub.db?_journal_mode=WAL")
if err != nil {
log.Fatal(err)
}
defer db.Close()
s := &Server{db: db}
mux := http.NewServeMux()
mux.HandleFunc("/api/analytics/views-by-region", s.viewsByRegion)
mux.HandleFunc("/api/analytics/trending-categories", s.trendingCategories)
mux.HandleFunc("/api/analytics/peak-hours", s.peakHours)
log.Println("Analytics server on :8080")
log.Fatal(http.ListenAndServe(":8080", corsMiddleware(mux)))
}
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "https://topvideohub.com")
w.Header().Set("Content-Type", "application/json")
next.ServeHTTP(w, r)
})
}
Views by Region Endpoint
type RegionStat struct {
Region string `json:"region"`
VideoCount int `json:"video_count"`
TotalViews int64 `json:"total_views"`
}
func (s *Server) viewsByRegion(w http.ResponseWriter, r *http.Request) {
rows, err := s.db.Query(`
SELECT vr.region,
COUNT(DISTINCT vr.video_id) AS video_count,
COALESCE(SUM(v.view_count), 0) AS total_views
FROM video_regions vr
JOIN videos v ON v.id = vr.video_id
GROUP BY vr.region
ORDER BY total_views DESC
`)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
defer rows.Close()
var stats []RegionStat
for rows.Next() {
var st RegionStat
rows.Scan(&st.Region, &st.VideoCount, &st.TotalViews)
stats = append(stats, st)
}
json.NewEncoder(w).Encode(stats)
}
Trending Categories Endpoint
type CategoryStat struct {
Name string `json:"name"`
VideoCount int `json:"video_count"`
SharePct float64 `json:"share_pct"`
}
func (s *Server) trendingCategories(w http.ResponseWriter, r *http.Request) {
region := r.URL.Query().Get("region")
query := `
SELECT c.name, COUNT(v.id) AS cnt,
ROUND(COUNT(v.id) * 100.0 / SUM(COUNT(v.id)) OVER (), 1) AS share
FROM videos v
JOIN categories c ON c.id = v.category_id`
args := []any{}
if region != "" {
query += " JOIN video_regions vr ON vr.video_id = v.id WHERE vr.region = ?"
args = append(args, region)
}
query += " GROUP BY c.name ORDER BY cnt DESC LIMIT 10"
rows, err := s.db.Query(query, args...)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
defer rows.Close()
var stats []CategoryStat
for rows.Next() {
var st CategoryStat
rows.Scan(&st.Name, &st.VideoCount, &st.SharePct)
stats = append(stats, st)
}
json.NewEncoder(w).Encode(stats)
}
Peak Hours Endpoint
type HourStat struct {
Hour int `json:"hour"`
Count int `json:"count"`
}
func (s *Server) peakHours(w http.ResponseWriter, r *http.Request) {
rows, err := s.db.Query(`
SELECT CAST(strftime('%H', fetched_at) AS INTEGER) AS hour, COUNT(*) AS cnt
FROM videos WHERE fetched_at > datetime('now', '-7 days')
GROUP BY hour ORDER BY hour
`)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
defer rows.Close()
stats := make([]HourStat, 0, 24)
for rows.Next() {
var h HourStat
rows.Scan(&h.Hour, &h.Count)
stats = append(stats, h)
}
json.NewEncoder(w).Encode(stats)
}
Chart.js Dashboard
async function loadAnalytics() {
const BASE = 'https://analytics.topvideohub.com';
const [regions, categories, hours] = await Promise.all([
fetch(`${BASE}/api/analytics/views-by-region`).then(r => r.json()),
fetch(`${BASE}/api/analytics/trending-categories?region=JP`).then(r => r.json()),
fetch(`${BASE}/api/analytics/peak-hours`).then(r => r.json()),
]);
new Chart(document.getElementById('regionChart'), {
type: 'bar',
data: {
labels: regions.map(r => r.region),
datasets: [{ label: 'Total Views', data: regions.map(r => r.total_views),
backgroundColor: 'rgba(99,102,241,0.7)' }],
},
options: { indexAxis: 'y' },
});
new Chart(document.getElementById('categoryChart'), {
type: 'doughnut',
data: { labels: categories.map(c => c.name),
datasets: [{ data: categories.map(c => c.share_pct) }] },
});
new Chart(document.getElementById('peakChart'), {
type: 'line',
data: { labels: hours.map(h => `${h.hour}:00`),
datasets: [{ label: 'Fetched Videos', data: hours.map(h => h.count),
tension: 0.3, fill: true }] },
});
}
loadAnalytics();
For TopVideoHub, the peak hours chart consistently shows spikes at 19-22 JST as Japanese audiences come home and browse trending content.
This article is part of the Building TopVideoHub series. Check out TopVideoHub to see these techniques in action.
Top comments (0)