Files
MeBox/internal/service/storage.go
T
truewhile b0fe40142a Rebrand MMTL to MeBox (name, logo, Docker image) (#17)
* Rebrand MMTL to MeBox across codebase and assets

Rename the project display name, Go module path, environment variable
prefix (MEBOX_*), Docker image references, and UI branding from MMTL/mmtl
to MeBox/mebox. Replace logo assets with the new MeBox icon and keep
legacy SQLite migration support for existing mmtl.db deployments.

Co-authored-by: truewhile <truewhile@users.noreply.github.com>

* Fix logo icons: use cube-only crop without truncated text

Previous icon generation cropped too much of the source image, including
partial MeBox wordmark text that was cut off in square icon containers.
Regenerate logo-64/192/512, favicon, and SVG from cube-only region.

Co-authored-by: truewhile <truewhile@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: truewhile <truewhile@users.noreply.github.com>
2026-09-02 16:26:28 +08:00

117 lines
3.2 KiB
Go

// Package service — disk usage breakdown.
//
// StorageService aggregates "how much disk does each library use" for
// the React Storage tab. Numbers are computed from the in-DB
// media.size_bytes column so we never hit the disk on the hot path.
package service
import (
"context"
"go.uber.org/zap"
"github.com/truewhile/MeBox/internal/repository"
)
// StorageService is the read-only aggregator.
type StorageService struct {
log *zap.Logger
repo *repository.Container
}
// NewStorageService is the constructor.
func NewStorageService(log *zap.Logger, repo *repository.Container) *StorageService {
return &StorageService{log: log, repo: repo}
}
// Breakdown is what /api/storage returns.
type Breakdown struct {
TotalBytes int64 `json:"total_bytes"`
TotalSeconds int64 `json:"total_seconds"`
ByLibrary []LibraryUsage `json:"by_library"`
ByContainer []ContainerStat `json:"by_container"`
}
// LibraryUsage is per-library disk + duration totals.
type LibraryUsage struct {
LibraryID string `json:"library_id"`
Name string `json:"name"`
Type string `json:"type"`
Path string `json:"path"`
MediaCount int64 `json:"media_count"`
TotalBytes int64 `json:"total_bytes"`
TotalSeconds int64 `json:"total_seconds"`
}
// ContainerStat counts media items per container (mp4 / mkv / …).
type ContainerStat struct {
Container string `json:"container"`
Count int64 `json:"count"`
Bytes int64 `json:"bytes"`
}
// Compute returns the full breakdown.
func (s *StorageService) Compute(ctx context.Context) (*Breakdown, error) {
libs, err := s.repo.Library.List(ctx)
if err != nil {
return nil, err
}
libs = NormalizeCloudLibraryDisplay(libs)
out := &Breakdown{ByLibrary: make([]LibraryUsage, 0, len(libs))}
for _, l := range libs {
var usage LibraryUsage
usage.LibraryID = l.ID
usage.Name = l.Name
usage.Type = l.Type
usage.Path = l.Path
row := struct {
Count int64
Size int64
Seconds int64
}{}
err := s.repo.DB.WithContext(ctx).
Table("media").
Where("library_id = ? AND deleted_at IS NULL", l.ID).
Select("COUNT(*) as count, COALESCE(SUM(size_bytes),0) as size, COALESCE(SUM(duration_sec),0) as seconds").
Scan(&row).Error
if err != nil {
return nil, err
}
usage.MediaCount = row.Count
usage.TotalBytes = row.Size
usage.TotalSeconds = row.Seconds
out.TotalBytes += row.Size
out.TotalSeconds += row.Seconds
out.ByLibrary = append(out.ByLibrary, usage)
}
rows, err := s.containerStats(ctx)
if err != nil {
return nil, err
}
out.ByContainer = rows
return out, nil
}
func (s *StorageService) containerStats(ctx context.Context) ([]ContainerStat, error) {
rows, err := s.repo.DB.WithContext(ctx).
Table("media").
Where("deleted_at IS NULL").
Select("COALESCE(NULLIF(container,''),'unknown') as container, COUNT(*) as count, COALESCE(SUM(size_bytes),0) as bytes").
Group("container").
Rows()
if err != nil {
return nil, err
}
defer rows.Close()
out := []ContainerStat{}
for rows.Next() {
var c ContainerStat
if err := rows.Scan(&c.Container, &c.Count, &c.Bytes); err != nil {
return nil, err
}
out = append(out, c)
}
return out, nil
}