Files
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

179 lines
4.2 KiB
Go

// Package service — filesystem scanner.
//
// ScannerService walks the configured library roots looking for video
// files, then upserts a model.Media row per file. Each upsert also runs
// ffprobe (when available) and queues a metadata lookup for newly added
// rows.
//
// When a filename exposes season + episode numbers we store them on the
// Media row for every library type, so variety shows and other episodic
// collections get the same grouping experience as TV/anime.
package service
import (
"errors"
"strings"
"sync"
"go.uber.org/zap"
"github.com/truewhile/MeBox/internal/config"
"github.com/truewhile/MeBox/internal/repository"
)
// videoExtensions lists the file extensions treated as media. Matches the
// legacy Python defaults.
var videoExtensions = map[string]struct{}{
".mkv": {},
".mp4": {},
".m4v": {},
".avi": {},
".mov": {},
".webm": {},
".flv": {},
".wmv": {},
".ts": {},
".m2ts": {},
".mts": {},
".vob": {},
".rmvb": {},
".rm": {},
".3gp": {},
".mpg": {},
".mpeg": {},
".iso": {},
".strm": {},
}
func mediaExtensionSupportsProbe(ext string) bool {
return ext != ".strm" && ext != ".iso"
}
// ScannerService walks libraries on disk and upserts model.Media rows.
type ScannerService struct {
cfg *config.Config
log *zap.Logger
repo *repository.Container
hub *Hub
probe *FFprobeService
scraper *ScraperService
organizer *OrganizerService
cache *RuntimeCacheService
imageProxy *ImageProxy
localMediaProbeOnce sync.Once
localMediaProbeQueue chan localMediaProbeTask
localMediaProbeMu sync.Mutex
localMediaProbing map[string]struct{}
localScanMu sync.Mutex
localScans map[string]struct{}
}
// NewScannerService is the constructor.
func NewScannerService(
cfg *config.Config,
log *zap.Logger,
repo *repository.Container,
hub *Hub,
probe *FFprobeService,
scraper *ScraperService,
) *ScannerService {
return &ScannerService{
cfg: cfg, log: log, repo: repo, hub: hub,
probe: probe,
scraper: scraper,
localMediaProbeQueue: make(chan localMediaProbeTask, 1024),
localMediaProbing: make(map[string]struct{}),
localScans: make(map[string]struct{}),
}
}
func (s *ScannerService) SetOrganizer(organizer *OrganizerService) {
if s != nil {
s.organizer = organizer
}
}
func (s *ScannerService) SetRuntimeCache(cache *RuntimeCacheService) {
if s != nil {
s.cache = cache
}
}
func (s *ScannerService) SetImageProxy(imageProxy *ImageProxy) {
s.imageProxy = imageProxy
}
// ScanResult summarises a scan run.
type ScanResult struct {
LibraryID string `json:"library_id"`
Visited int `json:"visited"`
Added int `json:"added"`
Updated int `json:"updated"`
Skipped int `json:"skipped"`
Probed int `json:"probed"`
LocalMetadata int `json:"local_metadata"`
Removed int64 `json:"removed"`
ErrorCount int `json:"error_count,omitempty"`
Errors []string `json:"errors,omitempty"`
}
var ErrLocalScanAlreadyRunning = errors.New("local scan already running")
const maxScanErrorDetails = 20
func addScanError(res *ScanResult, path string, err error) {
if res == nil || err == nil {
return
}
res.ErrorCount++
if len(res.Errors) >= maxScanErrorDetails {
return
}
path = strings.TrimSpace(path)
msg := strings.TrimSpace(err.Error())
if path != "" {
msg = path + ": " + msg
}
res.Errors = append(res.Errors, msg)
}
type localMediaProbeTask struct {
path string
}
type existingLocalMedia struct {
LibraryRootID string
RelativePath string
Title string
OriginalName string
EpisodeTitle string
SizeBytes int64
DurationSec int
Width int
Height int
VideoCodec string
AudioCodec string
Container string
STRMURL string
FileID string
PosterURL string
BackdropURL string
Overview string
Year int
ReleaseDate string
Rating float32
TMDbID int
BangumiID int
DoubanID string
TheTVDBID string
SeasonNum int
EpisodeNum int
Genres string
Countries string
Languages string
NSFW bool
ScrapeStatus string
}