Files
silo-server/internal/metadata/nfo/nfo.go
T

157 lines
3.7 KiB
Go

package nfo
import (
"bytes"
"encoding/xml"
"fmt"
"strings"
)
// nfoUniqueID represents a <uniqueid> element inside an NFO XML file.
type nfoUniqueID struct {
Type string `xml:"type,attr"`
Default string `xml:"default,attr"`
Value string `xml:",chardata"`
}
// nfoMovie represents the <movie> root element.
type nfoMovie struct {
XMLName xml.Name `xml:"movie"`
Title string `xml:"title"`
Year int `xml:"year"`
Plot string `xml:"plot"`
UniqueIDs []nfoUniqueID `xml:"uniqueid"`
}
// nfoTVShow represents the <tvshow> root element.
type nfoTVShow struct {
XMLName xml.Name `xml:"tvshow"`
Title string `xml:"title"`
Year int `xml:"year"`
Plot string `xml:"plot"`
UniqueIDs []nfoUniqueID `xml:"uniqueid"`
}
// nfoEpisode represents the <episodedetails> root element.
type nfoEpisode struct {
XMLName xml.Name `xml:"episodedetails"`
Title string `xml:"title"`
Season int `xml:"season"`
Episode int `xml:"episode"`
Plot string `xml:"plot"`
UniqueIDs []nfoUniqueID `xml:"uniqueid"`
}
// parsedNFO holds extracted data from an NFO file.
type parsedNFO struct {
Title string
Year int
Overview string
TmdbID string
ImdbID string
TvdbID string
Type string // movie, series, episode
Season int
Episode int
}
// parseNFOData parses NFO XML data and returns structured results.
func parseNFOData(data []byte) (*parsedNFO, error) {
// Strip UTF-8 BOM if present.
data = bytes.TrimPrefix(data, []byte{0xEF, 0xBB, 0xBF})
rootTag, err := detectRootTag(data)
if err != nil {
return nil, fmt.Errorf("nfo: %w", err)
}
switch rootTag {
case "movie":
return parseMovieNFO(data)
case "tvshow":
return parseTVShowNFO(data)
case "episodedetails":
return parseEpisodeNFO(data)
default:
return nil, fmt.Errorf("nfo: unsupported root element <%s>", rootTag)
}
}
func detectRootTag(data []byte) (string, error) {
decoder := xml.NewDecoder(bytes.NewReader(data))
for {
tok, err := decoder.Token()
if err != nil {
return "", fmt.Errorf("cannot detect root element: %w", err)
}
if se, ok := tok.(xml.StartElement); ok {
return se.Name.Local, nil
}
}
}
func parseMovieNFO(data []byte) (*parsedNFO, error) {
var m nfoMovie
if err := xmlUnmarshal(data, &m); err != nil {
return nil, fmt.Errorf("nfo: failed to parse movie: %w", err)
}
p := &parsedNFO{
Title: strings.TrimSpace(m.Title),
Year: m.Year,
Overview: strings.TrimSpace(m.Plot),
Type: "movie",
}
applyUniqueIDs(p, m.UniqueIDs)
return p, nil
}
func parseTVShowNFO(data []byte) (*parsedNFO, error) {
var s nfoTVShow
if err := xmlUnmarshal(data, &s); err != nil {
return nil, fmt.Errorf("nfo: failed to parse tvshow: %w", err)
}
p := &parsedNFO{
Title: strings.TrimSpace(s.Title),
Year: s.Year,
Overview: strings.TrimSpace(s.Plot),
Type: "series",
}
applyUniqueIDs(p, s.UniqueIDs)
return p, nil
}
func parseEpisodeNFO(data []byte) (*parsedNFO, error) {
var e nfoEpisode
if err := xmlUnmarshal(data, &e); err != nil {
return nil, fmt.Errorf("nfo: failed to parse episodedetails: %w", err)
}
p := &parsedNFO{
Title: strings.TrimSpace(e.Title),
Overview: strings.TrimSpace(e.Plot),
Type: "episode",
Season: e.Season,
Episode: e.Episode,
}
applyUniqueIDs(p, e.UniqueIDs)
return p, nil
}
func xmlUnmarshal(data []byte, v any) error {
decoder := xml.NewDecoder(bytes.NewReader(data))
return decoder.Decode(v)
}
func applyUniqueIDs(p *parsedNFO, ids []nfoUniqueID) {
for _, uid := range ids {
val := strings.TrimSpace(uid.Value)
switch strings.ToLower(uid.Type) {
case "imdb":
p.ImdbID = val
case "tmdb":
p.TmdbID = val
case "tvdb":
p.TvdbID = val
}
}
}