156 lines
4.6 KiB
Go
156 lines
4.6 KiB
Go
package jellycompat
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
"strconv"
|
|
"sync"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// EncodedIDType distinguishes packed compat UUIDs.
|
|
type EncodedIDType byte
|
|
|
|
const (
|
|
EncodedIDLibrary EncodedIDType = 1
|
|
EncodedIDItem EncodedIDType = 2
|
|
EncodedIDMediaSource EncodedIDType = 3
|
|
EncodedIDSeason EncodedIDType = 4
|
|
EncodedIDPlaySession EncodedIDType = 5
|
|
EncodedIDGenre EncodedIDType = 6
|
|
EncodedIDStudio EncodedIDType = 7
|
|
EncodedIDPerson EncodedIDType = 8
|
|
)
|
|
|
|
var (
|
|
pseudoUserNamespace = uuid.MustParse("3dfcc388-bf95-5572-bc16-7f1a375992dd")
|
|
stringIDNamespaces = map[EncodedIDType]uuid.UUID{
|
|
EncodedIDItem: uuid.MustParse("0b6716ca-1f61-5987-b17b-f592f04fd6b3"),
|
|
EncodedIDSeason: uuid.MustParse("29831b2b-dad5-5a85-b506-4d1fb2da01ed"),
|
|
EncodedIDPlaySession: uuid.MustParse("75a69ca8-f95f-5e9d-ac0a-d34a37b93eb4"),
|
|
EncodedIDGenre: uuid.MustParse("c0cbb8ea-8331-52c0-b160-15e7cf899fb0"),
|
|
EncodedIDStudio: uuid.MustParse("23712982-b769-592d-9360-b4d3f39654db"),
|
|
EncodedIDPerson: uuid.MustParse("a4e7c1d6-3b8f-5a2e-9c01-7d6f4e8b2a13"),
|
|
}
|
|
)
|
|
|
|
// DecodedID is a packed compat UUID decoded back to its type and value.
|
|
type DecodedID struct {
|
|
Type EncodedIDType
|
|
Value uint64
|
|
}
|
|
|
|
// ResourceIDCodec encodes numeric IDs directly and keeps reversible mappings
|
|
// for opaque string content IDs used by media items and seasons.
|
|
type ResourceIDCodec struct {
|
|
mu sync.RWMutex
|
|
reverse map[string]registeredID
|
|
mediaSourceOwners map[int64]string
|
|
}
|
|
|
|
type registeredID struct {
|
|
kind EncodedIDType
|
|
value string
|
|
}
|
|
|
|
// PseudoUserID deterministically derives the Jellyfin pseudo-user UUID.
|
|
func PseudoUserID(userID int, profileID string) uuid.UUID {
|
|
return uuid.NewSHA1(pseudoUserNamespace, fmt.Appendf(nil, "%d:%s", userID, profileID))
|
|
}
|
|
|
|
// NewResourceIDCodec creates a new route ID codec.
|
|
func NewResourceIDCodec() *ResourceIDCodec {
|
|
return &ResourceIDCodec{
|
|
reverse: make(map[string]registeredID),
|
|
mediaSourceOwners: make(map[int64]string),
|
|
}
|
|
}
|
|
|
|
// EncodeNumericID packs a numeric Silo identifier into a UUID.
|
|
func EncodeNumericID(kind EncodedIDType, value uint64) uuid.UUID {
|
|
var raw [16]byte
|
|
raw[0] = byte(kind)
|
|
binary.BigEndian.PutUint64(raw[8:], value)
|
|
return uuid.UUID(raw)
|
|
}
|
|
|
|
// EncodeStringID encodes a Silo identifier into a Jellyfin UUID string.
|
|
func (c *ResourceIDCodec) EncodeStringID(kind EncodedIDType, value string) string {
|
|
if numeric, err := strconv.ParseUint(value, 10, 64); err == nil {
|
|
return EncodeNumericID(kind, numeric).String()
|
|
}
|
|
|
|
namespace, ok := stringIDNamespaces[kind]
|
|
if !ok {
|
|
namespace = uuid.NameSpaceURL
|
|
}
|
|
encoded := uuid.NewSHA1(namespace, []byte(value))
|
|
|
|
c.mu.Lock()
|
|
c.reverse[encoded.String()] = registeredID{kind: kind, value: value}
|
|
c.mu.Unlock()
|
|
|
|
return encoded.String()
|
|
}
|
|
|
|
// EncodeIntID encodes a native integer ID into a Jellyfin UUID string.
|
|
func (c *ResourceIDCodec) EncodeIntID(kind EncodedIDType, value int64) string {
|
|
return EncodeNumericID(kind, uint64(value)).String()
|
|
}
|
|
|
|
// DecodeStringID decodes a compat UUID back to the original native string ID.
|
|
func (c *ResourceIDCodec) DecodeStringID(kind EncodedIDType, raw string) (string, error) {
|
|
if decoded, err := DecodeID(raw); err == nil && decoded.Type == kind {
|
|
return strconv.FormatUint(decoded.Value, 10), nil
|
|
}
|
|
|
|
c.mu.RLock()
|
|
registered, ok := c.reverse[raw]
|
|
c.mu.RUnlock()
|
|
if !ok || registered.kind != kind {
|
|
return "", fmt.Errorf("unknown compat id %q", raw)
|
|
}
|
|
return registered.value, nil
|
|
}
|
|
|
|
// DecodeIntID decodes a compat UUID back to a native integer ID.
|
|
func (c *ResourceIDCodec) DecodeIntID(kind EncodedIDType, raw string) (int64, error) {
|
|
decoded, err := DecodeID(raw)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if decoded.Type != kind {
|
|
return 0, fmt.Errorf("unexpected compat id type %d", decoded.Type)
|
|
}
|
|
return int64(decoded.Value), nil
|
|
}
|
|
|
|
// RegisterMediaSourceOwner records which content item owns a media-source/file ID.
|
|
func (c *ResourceIDCodec) RegisterMediaSourceOwner(fileID int64, contentID string) {
|
|
c.mu.Lock()
|
|
c.mediaSourceOwners[fileID] = contentID
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
// LookupMediaSourceOwner resolves a media-source/file ID back to its content item.
|
|
func (c *ResourceIDCodec) LookupMediaSourceOwner(fileID int64) (string, bool) {
|
|
c.mu.RLock()
|
|
contentID, ok := c.mediaSourceOwners[fileID]
|
|
c.mu.RUnlock()
|
|
return contentID, ok
|
|
}
|
|
|
|
// DecodeID unpacks a compat UUID into its original numeric value.
|
|
func DecodeID(raw string) (DecodedID, error) {
|
|
parsed, err := uuid.Parse(raw)
|
|
if err != nil {
|
|
return DecodedID{}, fmt.Errorf("parse uuid: %w", err)
|
|
}
|
|
|
|
return DecodedID{
|
|
Type: EncodedIDType(parsed[0]),
|
|
Value: binary.BigEndian.Uint64(parsed[8:]),
|
|
}, nil
|
|
}
|