mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 11:16:37 +08:00
8824638efb
Backend
- service/transcoder.go: encoder selector (software / nvenc / qsv /
vaapi) with proper hwaccel + scale_* filters per encoder; configurable
bitrate/preset/height/segment-seconds via transcoder.* config; new
Active() snapshot + ActiveJob struct for the Tasks panel; unit-tested
via buildFFmpegArgs(...).
- service/thetvdb.go: TheTVDB v4 provider — login() caches the JWT for
24h, SearchSeries() returns Match struct.
- service/fanart.go: Fanart.tv provider — high-res movie artwork keyed
by TMDb id; used to upgrade poster/backdrop after a successful match.
- service/scraper.go: provider chain reworked — anime → Bangumi, tv →
TheTVDB, default → TMDb, with optional Fanart upgrade post-match.
- service/discover.go: TMDb /trending/movie/day + /movie/popular for
the Discover rail.
- service/ai.go: OpenAI-compatible client. SmartSearch() turns a free-
form query into a structured SearchIntent JSON; Recommend() emits a
short list of titles given the user's history. Disabled when
ai.api_key is empty.
- service/nfo.go: per-movie .nfo writer (Kodi/Jellyfin schema) +
library-scope batch exporter.
- service/media.go: SoftDelete / Restore / Purge / ListRecycleBin
helpers backed by gorm Unscoped().
- service/service.go: container wires Fanart, TheTVDB, Discover, AI,
NFO; everything still tears down cleanly via Close().
- config: new transcoder.* section (encoder, preset, video_bitrate,
max_rate, buf_size, max_height, segment_seconds) with sensible
defaults.
Handlers / routes
- new files: discover.go, recycle.go, nfo.go, ai.go, tasks.go.
- registers GET /tasks, /discover/{trending,popular},
/ai/{status,search,recommend}; admin-only GET /recycle and
DELETE/POST /media/:id (soft delete / restore / purge); admin-only
POST /media/:id/nfo and /libraries/:id/nfo.
Frontend
- new pages: DiscoverPage, TasksPage, RecycleBinPage; SearchPage gains
an optional AI smart-search toggle that calls /api/ai/search.
- MediaDetailPage gains admin actions for 导出 NFO and 移至回收站.
- new api/ helpers: ai.ts, discover.ts, recycle.ts, tasks.ts.
- App.tsx routes /discover, /tasks, /recycle (last two admin-only).
- Layout sidebar groups now include Discover + Tasks + Recycle Bin.
Docker / config
- Dockerfile: adds intel-media-driver / libva-utils / mesa-va-gallium
so QSV + VAAPI work out of the box on Intel iGPUs.
- docker-compose.yml: documents devices + group_add + nvidia runtime
overrides for hardware transcoding.
Verified: go build, go vet, go test (incl. new TestBuildFFmpegArgs across
software / nvenc / qsv / vaapi encoder profiles); tsc -b && vite build
emits 22 route chunks plus the deferred hls chunk; main bundle 248 KB /
83 KB gzipped.
116 lines
3.2 KiB
Go
116 lines
3.2 KiB
Go
// Package service — NFO writer (Kodi / Jellyfin compatibility).
|
|
//
|
|
// Kodi and Jellyfin index media using sidecar XML files alongside the
|
|
// source video. We export a minimal subset that those scrapers consume
|
|
// happily:
|
|
//
|
|
// movie.mkv -> movie.nfo (<movie>...</movie>)
|
|
// tvshow/ -> tvshow.nfo (<tvshow>...</tvshow>) [future]
|
|
//
|
|
// Today only the per-movie writer is implemented; the per-show / per-episode
|
|
// writers are stubbed with TODO markers.
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/xml"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/ShukeBta/MediaStationGo/internal/repository"
|
|
)
|
|
|
|
// NFOService is the entry point used by the admin "导出 NFO" action.
|
|
type NFOService struct {
|
|
log *zap.Logger
|
|
repo *repository.Container
|
|
}
|
|
|
|
// NewNFOService is the constructor.
|
|
func NewNFOService(log *zap.Logger, repo *repository.Container) *NFOService {
|
|
return &NFOService{log: log, repo: repo}
|
|
}
|
|
|
|
// movieNFO is the on-disk schema. Tag names match Kodi's expectations.
|
|
type movieNFO struct {
|
|
XMLName xml.Name `xml:"movie"`
|
|
Title string `xml:"title"`
|
|
Original string `xml:"originaltitle,omitempty"`
|
|
Year int `xml:"year,omitempty"`
|
|
Plot string `xml:"plot,omitempty"`
|
|
Rating float32 `xml:"rating,omitempty"`
|
|
Poster string `xml:"thumb,omitempty"`
|
|
Fanart string `xml:"fanart,omitempty"`
|
|
TMDb int `xml:"tmdbid,omitempty"`
|
|
}
|
|
|
|
// ExportOne writes a movie.nfo file next to the media file. Existing files
|
|
// are overwritten so a re-scrape always reflects the latest metadata.
|
|
func (s *NFOService) ExportOne(ctx context.Context, mediaID string) (string, error) {
|
|
m, err := s.repo.Media.FindByID(ctx, mediaID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if m == nil {
|
|
return "", errors.New("media not found")
|
|
}
|
|
if m.Path == "" {
|
|
return "", errors.New("media has empty path")
|
|
}
|
|
|
|
doc := movieNFO{
|
|
Title: m.Title,
|
|
Original: m.OriginalName,
|
|
Year: m.Year,
|
|
Plot: m.Overview,
|
|
Rating: m.Rating,
|
|
Poster: m.PosterURL,
|
|
Fanart: m.BackdropURL,
|
|
TMDb: m.TMDbID,
|
|
}
|
|
out, err := xml.MarshalIndent(doc, "", " ")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
body := []byte(xml.Header + string(out) + "\n")
|
|
|
|
dst := nfoPath(m.Path)
|
|
if err := os.WriteFile(dst, body, 0o644); err != nil {
|
|
return "", err
|
|
}
|
|
s.log.Info("nfo exported", zap.String("media_id", m.ID), zap.String("path", dst))
|
|
return dst, nil
|
|
}
|
|
|
|
// ExportLibrary loops through every matched movie in a library and writes
|
|
// an .nfo for each. Returns (written, error).
|
|
func (s *NFOService) ExportLibrary(ctx context.Context, libraryID string) (int, error) {
|
|
type row struct{ ID string }
|
|
var ids []row
|
|
q := s.repo.DB.Table("media").Select("id").Where("scrape_status = ?", "matched")
|
|
if libraryID != "" {
|
|
q = q.Where("library_id = ?", libraryID)
|
|
}
|
|
if err := q.Scan(&ids).Error; err != nil {
|
|
return 0, err
|
|
}
|
|
written := 0
|
|
for _, r := range ids {
|
|
if _, err := s.ExportOne(ctx, r.ID); err == nil {
|
|
written++
|
|
}
|
|
}
|
|
return written, nil
|
|
}
|
|
|
|
func nfoPath(media string) string {
|
|
dir := filepath.Dir(media)
|
|
base := strings.TrimSuffix(filepath.Base(media), filepath.Ext(media))
|
|
return filepath.Join(dir, fmt.Sprintf("%s.nfo", base))
|
|
}
|