c8f9832f48
A panic would occur if a field in the config file (e.g. youtube-dl.youtube-dl-path) was not set
209 lines
4.4 KiB
Go
209 lines
4.4 KiB
Go
package extractor
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
ErrNoSearchResults = errors.New("no search results")
|
|
ErrNoSearchProvider = errors.New("no search provider available")
|
|
ErrNoSuggestionProvider = errors.New("no search suggestion provider available")
|
|
)
|
|
|
|
var (
|
|
providers []provider
|
|
extractors []extractor
|
|
searchers []searcher
|
|
suggestors []suggestor
|
|
defaultConfig Config
|
|
)
|
|
|
|
func Extract(cfg Config, input string) ([]Data, error) {
|
|
if err := cfg.CheckValidity(); err != nil {
|
|
return nil, err
|
|
}
|
|
for _, e := range extractors {
|
|
if e.Matches(cfg[e.name], input) {
|
|
data, err := e.Extract(cfg[e.name], input)
|
|
if err != nil {
|
|
return nil, &Error{e.name, err}
|
|
}
|
|
return data, nil
|
|
}
|
|
}
|
|
d, err := Search(cfg, input)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(d) == 0 {
|
|
return nil, ErrNoSearchResults
|
|
}
|
|
return []Data{d[0]}, nil
|
|
}
|
|
|
|
func Search(cfg Config, input string) ([]Data, error) {
|
|
if err := cfg.CheckValidity(); err != nil {
|
|
return nil, err
|
|
}
|
|
for _, s := range searchers {
|
|
data, err := s.Search(cfg[s.name], input)
|
|
if err != nil {
|
|
return nil, &Error{s.name, err}
|
|
}
|
|
return data, nil
|
|
}
|
|
return nil, ErrNoSearchProvider
|
|
}
|
|
|
|
func Suggest(cfg Config, input string) ([]string, error) {
|
|
if err := cfg.CheckValidity(); err != nil {
|
|
return nil, err
|
|
}
|
|
for _, s := range suggestors {
|
|
data, err := s.Suggest(cfg[s.name], input)
|
|
if err != nil {
|
|
return nil, &Error{s.name, err}
|
|
}
|
|
return data, nil
|
|
}
|
|
return nil, ErrNoSuggestionProvider
|
|
}
|
|
|
|
type Error struct {
|
|
ProviderName string
|
|
Err error
|
|
}
|
|
|
|
func (e *Error) Error() string {
|
|
return "extractor[" + e.ProviderName + "]: " + e.Err.Error()
|
|
}
|
|
|
|
type provider struct {
|
|
Provider
|
|
name string
|
|
}
|
|
|
|
type extractor struct {
|
|
Extractor
|
|
name string
|
|
}
|
|
|
|
type searcher struct {
|
|
Searcher
|
|
name string
|
|
}
|
|
|
|
type suggestor struct {
|
|
Suggestor
|
|
name string
|
|
}
|
|
|
|
type Config map[string]ProviderConfig
|
|
|
|
func DefaultConfig() Config {
|
|
if defaultConfig == nil {
|
|
cfg := make(Config)
|
|
for _, e := range providers {
|
|
cfg[e.name] = e.DefaultConfig()
|
|
}
|
|
return cfg
|
|
} else {
|
|
return defaultConfig
|
|
}
|
|
}
|
|
|
|
func (cfg Config) CheckValidity() error {
|
|
for chkProvider, chkCfg := range DefaultConfig() {
|
|
if _, ok := cfg[chkProvider]; !ok {
|
|
return fmt.Errorf("extractor config for %v is nil", chkProvider)
|
|
}
|
|
for k, v := range chkCfg {
|
|
expected, got := reflect.TypeOf(v), reflect.TypeOf(cfg[chkProvider][k])
|
|
if got != expected {
|
|
return &ConfigTypeError{
|
|
Provider: chkProvider,
|
|
Key: k,
|
|
Expected: expected,
|
|
Got: got,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type ConfigTypeError struct {
|
|
Provider string
|
|
Key string
|
|
Expected reflect.Type
|
|
Got reflect.Type
|
|
}
|
|
|
|
func (e *ConfigTypeError) Error() string {
|
|
expectedName := "nil"
|
|
if e.Expected != nil {
|
|
expectedName = e.Expected.Name()
|
|
}
|
|
gotName := "nil"
|
|
if e.Got != nil {
|
|
gotName = e.Got.Name()
|
|
}
|
|
return "invalid extractor configuration: " + e.Provider + "." + e.Key + ": expected " + expectedName + " but got " + gotName
|
|
}
|
|
|
|
type ProviderConfig map[string]any
|
|
|
|
type Provider interface {
|
|
DefaultConfig() ProviderConfig
|
|
}
|
|
|
|
type Extractor interface {
|
|
Provider
|
|
Matches(cfg ProviderConfig, input string) bool
|
|
Extract(cfg ProviderConfig, input string) ([]Data, error)
|
|
}
|
|
|
|
func AddExtractor(name string, e Extractor) {
|
|
providers = append(providers, provider{e, name})
|
|
extractors = append(extractors, extractor{e, name})
|
|
}
|
|
|
|
type Searcher interface {
|
|
Provider
|
|
Search(cfg ProviderConfig, input string) ([]Data, error)
|
|
}
|
|
|
|
func AddSearcher(name string, s Searcher) {
|
|
providers = append(providers, provider{s, name})
|
|
searchers = append(searchers, searcher{s, name})
|
|
}
|
|
|
|
type Suggestor interface {
|
|
Provider
|
|
Suggest(cfg ProviderConfig, input string) ([]string, error)
|
|
}
|
|
|
|
func AddSuggestor(name string, s Suggestor) {
|
|
providers = append(providers, provider{s, name})
|
|
suggestors = append(suggestors, suggestor{s, name})
|
|
}
|
|
|
|
type Data struct {
|
|
// Each instance of this struct should be reconstructable by calling
|
|
// Extract() on the SourceUrl
|
|
// String values are "" if not present
|
|
SourceUrl string
|
|
StreamUrl string // may expire, see Expires
|
|
Title string
|
|
PlaylistUrl string
|
|
PlaylistTitle string
|
|
Description string
|
|
Uploader string
|
|
Duration int // in seconds; -1 if unknown
|
|
Expires time.Time // when StreamUrl expires
|
|
OfficialArtist bool // only for sites that have non-music (e.g. YouTube); search results only
|
|
}
|