191 lines
5.0 KiB
Go
191 lines
5.0 KiB
Go
package nfo
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/Silo-Server/silo-server/internal/metadata"
|
|
)
|
|
|
|
// Provider reads metadata from NFO sidecar files.
|
|
type Provider struct{}
|
|
|
|
func NewProvider() *Provider { return &Provider{} }
|
|
|
|
func (p *Provider) Slug() string { return "nfo" }
|
|
func (p *Provider) Name() string { return "NFO Files" }
|
|
func (p *Provider) ForTypes() []string { return []string{"movie", "series"} }
|
|
|
|
// Search extracts external IDs from NFO for matching.
|
|
func (p *Provider) Search(ctx context.Context, query metadata.SearchQuery) ([]metadata.SearchResult, error) {
|
|
nfoPath := findNFOForQuery(query)
|
|
if nfoPath == "" {
|
|
return nil, nil
|
|
}
|
|
data, err := os.ReadFile(nfoPath)
|
|
if err != nil {
|
|
return nil, nil // NFO read errors are not fatal
|
|
}
|
|
parsed, err := parseNFOData(data)
|
|
if err != nil {
|
|
return nil, nil // NFO parse errors are not fatal
|
|
}
|
|
if query.ContentType != "" && parsed.Type != "" && parsed.Type != query.ContentType {
|
|
return nil, nil
|
|
}
|
|
ids := make(map[string]string)
|
|
if parsed.TmdbID != "" {
|
|
ids["tmdb"] = parsed.TmdbID
|
|
}
|
|
if parsed.ImdbID != "" {
|
|
ids["imdb"] = parsed.ImdbID
|
|
}
|
|
if parsed.TvdbID != "" {
|
|
ids["tvdb"] = parsed.TvdbID
|
|
}
|
|
if len(ids) == 0 && parsed.Title == "" {
|
|
return nil, nil
|
|
}
|
|
return []metadata.SearchResult{{
|
|
Name: parsed.Title,
|
|
Year: parsed.Year,
|
|
ProviderIDs: ids,
|
|
Provider: p.Slug(),
|
|
}}, nil
|
|
}
|
|
|
|
// GetMetadata parses full metadata from NFO file.
|
|
func (p *Provider) GetMetadata(ctx context.Context, req metadata.MetadataRequest) (*metadata.MetadataResult, error) {
|
|
nfoPath := findNFOForRequest(req)
|
|
if nfoPath == "" {
|
|
return &metadata.MetadataResult{}, nil
|
|
}
|
|
data, err := os.ReadFile(nfoPath)
|
|
if err != nil {
|
|
return &metadata.MetadataResult{}, nil
|
|
}
|
|
parsed, err := parseNFOData(data)
|
|
if err != nil {
|
|
return &metadata.MetadataResult{}, nil
|
|
}
|
|
result := &metadata.MetadataResult{
|
|
HasMetadata: true,
|
|
Title: parsed.Title,
|
|
Year: parsed.Year,
|
|
Overview: parsed.Overview,
|
|
ProviderIDs: make(map[string]string),
|
|
}
|
|
if parsed.TmdbID != "" {
|
|
result.ProviderIDs["tmdb"] = parsed.TmdbID
|
|
}
|
|
if parsed.ImdbID != "" {
|
|
result.ProviderIDs["imdb"] = parsed.ImdbID
|
|
}
|
|
if parsed.TvdbID != "" {
|
|
result.ProviderIDs["tvdb"] = parsed.TvdbID
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func findNFOForQuery(query metadata.SearchQuery) string {
|
|
candidatePaths := candidateSidecarPaths(
|
|
query.FilePath,
|
|
query.RepresentativeFilePath,
|
|
query.AllGroupFilePaths,
|
|
query.PrimarySidecarSearchPaths,
|
|
query.ProviderIDs["_filepath"],
|
|
)
|
|
return findNFO(candidatePaths)
|
|
}
|
|
|
|
func findNFOForRequest(req metadata.MetadataRequest) string {
|
|
candidatePaths := candidateSidecarPaths(
|
|
req.FilePath,
|
|
req.RepresentativeFilePath,
|
|
req.AllGroupFilePaths,
|
|
req.PrimarySidecarSearchPaths,
|
|
)
|
|
return findNFO(candidatePaths)
|
|
}
|
|
|
|
func candidateSidecarPaths(primary string, representative string, groupFiles []string, searchPaths []string, extras ...string) []string {
|
|
candidates := make([]string, 0, 2+len(groupFiles)+len(searchPaths)+len(extras))
|
|
for _, path := range []string{primary, representative} {
|
|
if strings.TrimSpace(path) != "" {
|
|
candidates = append(candidates, path)
|
|
}
|
|
}
|
|
candidates = append(candidates, groupFiles...)
|
|
candidates = append(candidates, searchPaths...)
|
|
candidates = append(candidates, extras...)
|
|
return compactNFOPaths(candidates)
|
|
}
|
|
|
|
func compactNFOPaths(paths []string) []string {
|
|
seen := make(map[string]struct{}, len(paths))
|
|
out := make([]string, 0, len(paths))
|
|
for _, path := range paths {
|
|
clean := filepath.Clean(strings.TrimSpace(path))
|
|
if clean == "" || clean == "." {
|
|
continue
|
|
}
|
|
if _, ok := seen[clean]; ok {
|
|
continue
|
|
}
|
|
seen[clean] = struct{}{}
|
|
out = append(out, clean)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// findNFO locates the best candidate NFO file across media-file and directory
|
|
// search paths. File paths check legacy directory-level sidecars first, then a
|
|
// basename-matched sidecar in the same directory.
|
|
func findNFO(paths []string) string {
|
|
candidates := make([]string, 0, len(paths)*3)
|
|
for _, path := range paths {
|
|
candidates = append(candidates, nfoCandidatesForPath(path)...)
|
|
}
|
|
candidates = compactNFOPaths(candidates)
|
|
for _, c := range candidates {
|
|
if _, err := os.Stat(c); err == nil {
|
|
return c
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func nfoCandidatesForPath(path string) []string {
|
|
if path == "" {
|
|
return nil
|
|
}
|
|
info, err := os.Stat(path)
|
|
if err == nil {
|
|
if info.IsDir() {
|
|
return directoryLevelNFOCandidates(path)
|
|
}
|
|
return fileLevelNFOCandidates(path)
|
|
}
|
|
return append(fileLevelNFOCandidates(path), directoryLevelNFOCandidates(path)...)
|
|
}
|
|
|
|
func fileLevelNFOCandidates(path string) []string {
|
|
ext := filepath.Ext(path)
|
|
dir := filepath.Dir(path)
|
|
base := strings.TrimSuffix(filepath.Base(path), ext)
|
|
return []string{
|
|
filepath.Join(dir, "movie.nfo"),
|
|
filepath.Join(dir, "tvshow.nfo"),
|
|
filepath.Join(dir, base+".nfo"),
|
|
}
|
|
}
|
|
|
|
func directoryLevelNFOCandidates(path string) []string {
|
|
return []string{
|
|
filepath.Join(path, "movie.nfo"),
|
|
filepath.Join(path, "tvshow.nfo"),
|
|
}
|
|
}
|