package server import ( "bytes" "context" "fmt" "html/template" "log/slog" "net/http" "strings" "git.lastassault.de/speatzle/morffix/constants" "github.com/go-echarts/go-echarts/v2/charts" "github.com/go-echarts/go-echarts/v2/opts" "github.com/jackc/pgx/v5" ) type StatsDisplay struct { Charts []ChartData } type ChartData struct { Element template.HTML Script template.HTML } type PieValue struct { Name string Value int } type PieIntValue struct { Id int Value int } func generatePie(name string, data []opts.PieData) ChartData { pie := charts.NewPie() pie.SetGlobalOptions( charts.WithInitializationOpts(opts.Initialization{ Theme: "dark", BackgroundColor: "#111", }), charts.WithTitleOpts(opts.Title{ Title: name, })) pie.AddSeries(name, data).SetSeriesOptions( charts.WithLabelOpts( opts.Label{ Show: opts.Bool(true), Formatter: "{b}: {c}", }), ) snippet := pie.RenderSnippet() return ChartData{ Element: template.HTML(snippet.Element), Script: template.HTML(snippet.Script), } } func generateStats(ctx context.Context) ([]ChartData, error) { data := []ChartData{} rows, err := db.Query(ctx, `SELECT COALESCE(jsonb_path_query_first(ffprobe_data, '$.streams[*] ? (@.codec_type == "video") ? (@.disposition.attached_pic == 0).codec_name')::text, 'Unknown') AS name, COUNT(*) AS value FROM files WHERE ffprobe_data IS NOT NULL GROUP BY jsonb_path_query_first(ffprobe_data, '$.streams[*] ? (@.codec_type == "video") ? (@.disposition.attached_pic == 0).codec_name');`) if err != nil { return nil, fmt.Errorf("Query Codecs: %w", err) } codecCounts, err := pgx.CollectRows(rows, pgx.RowToStructByName[PieValue]) if err != nil { return nil, fmt.Errorf("Collect Codec Data: %w", err) } res := []opts.PieData{} for _, v := range codecCounts { res = append(res, opts.PieData{ Name: strings.ReplaceAll(v.Name, "\"", ""), Value: v.Value, }) } data = append(data, generatePie("Codecs", res)) rows, err = db.Query(ctx, `SELECT COALESCE(jsonb_path_query_first(ffprobe_data, '$.streams[*] ? (@.codec_type == "video") ? (@.disposition.attached_pic == 0).width')::text, 'Unknown') AS name, COUNT(*) AS value FROM files WHERE ffprobe_data IS NOT NULL GROUP BY jsonb_path_query_first(ffprobe_data, '$.streams[*] ? (@.codec_type == "video") ? (@.disposition.attached_pic == 0).width');`) if err != nil { return nil, fmt.Errorf("Query Resolution: %w", err) } resolutionCounts, err := pgx.CollectRows(rows, pgx.RowToStructByName[PieValue]) if err != nil { return nil, fmt.Errorf("Collect Codec Data: %w", err) } res = []opts.PieData{} for _, v := range resolutionCounts { res = append(res, opts.PieData{ Name: strings.ReplaceAll(v.Name, "\"", ""), Value: v.Value, }) } data = append(data, generatePie("Resolution", res)) rows, err = db.Query(ctx, `SELECT COALESCE(jsonb_path_query_first(ffprobe_data, '$.format.format_name')::text, 'Unknown') AS name, COUNT(*) AS value FROM files WHERE ffprobe_data IS NOT NULL GROUP BY jsonb_path_query_first(ffprobe_data, '$.format.format_name');`) if err != nil { return nil, fmt.Errorf("Query Container: %w", err) } containerCounts, err := pgx.CollectRows(rows, pgx.RowToStructByName[PieValue]) if err != nil { return nil, fmt.Errorf("Collect Container Data: %w", err) } res = []opts.PieData{} for _, v := range containerCounts { res = append(res, opts.PieData{ Name: strings.ReplaceAll(v.Name, "\"", ""), Value: v.Value, }) } data = append(data, generatePie("Container", res)) rows, err = db.Query(ctx, `SELECT health AS id, COUNT(*) AS value FROM files GROUP BY health;`) if err != nil { return nil, fmt.Errorf("Query Health: %w", err) } healthCounts, err := pgx.CollectRows(rows, pgx.RowToStructByName[PieIntValue]) if err != nil { return nil, fmt.Errorf("Collect Health Data: %w", err) } res = []opts.PieData{} for _, v := range healthCounts { res = append(res, opts.PieData{ Name: constants.FileHealth(v.Id).String(), Value: v.Value, }) } data = append(data, generatePie("Health", res)) rows, err = db.Query(ctx, `SELECT transcode AS id, COUNT(*) AS value FROM files GROUP BY transcode;`) if err != nil { return nil, fmt.Errorf("Query Transcode: %w", err) } transcodeCounts, err := pgx.CollectRows(rows, pgx.RowToStructByName[PieIntValue]) if err != nil { return nil, fmt.Errorf("Collect Transcode Data: %w", err) } res = []opts.PieData{} for _, v := range transcodeCounts { res = append(res, opts.PieData{ Name: constants.FileTranscode(v.Id).String(), Value: v.Value, }) } data = append(data, generatePie("Transcode Status", res)) rows, err = db.Query(ctx, `SELECT status AS id, COUNT(*) AS value FROM tasks GROUP BY status;`) if err != nil { return nil, fmt.Errorf("Query Task Status: %w", err) } taskStatusCounts, err := pgx.CollectRows(rows, pgx.RowToStructByName[PieIntValue]) if err != nil { return nil, fmt.Errorf("Collect Task Status Data: %w", err) } res = []opts.PieData{} for _, v := range taskStatusCounts { res = append(res, opts.PieData{ Name: constants.TaskStatus(v.Id).String(), Value: v.Value, }) } data = append(data, generatePie("Task Status", res)) return data, nil } func handleStats(w http.ResponseWriter, r *http.Request) { data, err := generateStats(r.Context()) if err != nil { slog.ErrorContext(r.Context(), "Generate Stats:", "err", err) http.Error(w, "Generate Stats: "+err.Error(), http.StatusInternalServerError) return } buf := bytes.Buffer{} err = templates.ExecuteTemplate(&buf, constants.STATS_TEMPLATE_NAME, StatsDisplay{ Charts: data, }) if err != nil { slog.ErrorContext(r.Context(), "Executing Stats Template", "err", err) http.Error(w, "Error Executing Template: "+err.Error(), http.StatusInternalServerError) return } _, err = w.Write(buf.Bytes()) if err != nil { slog.ErrorContext(r.Context(), "Writing http Response", "err", err) } }