2022-09-20 00:54:22 +02:00
|
|
|
package extractor
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"reflect"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
2022-09-20 17:42:15 +02:00
|
|
|
ErrNoSearchResults = errors.New("no search results")
|
2022-09-20 00:54:22 +02:00
|
|
|
ErrNoSearchProvider = errors.New("no search provider available")
|
|
|
|
ErrNoSuggestionProvider = errors.New("no search suggestion provider available")
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
2022-09-20 17:43:47 +02:00
|
|
|
providers []provider
|
|
|
|
extractors []extractor
|
|
|
|
searchers []searcher
|
|
|
|
suggestors []suggestor
|
2022-09-20 00:54:22 +02:00
|
|
|
defaultConfig Config
|
|
|
|
)
|
|
|
|
|
|
|
|
func Extract(cfg Config, input string) ([]Data, error) {
|
|
|
|
if err := cfg.CheckTypes(); 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.CheckTypes(); 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.CheckTypes(); 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) CheckTypes() error {
|
|
|
|
for provider, pCfg := range cfg {
|
|
|
|
if pCfg == nil {
|
|
|
|
return fmt.Errorf("extractor config for %v is nil", provider)
|
|
|
|
}
|
|
|
|
for k, v := range pCfg {
|
|
|
|
got, expected := reflect.TypeOf(v), reflect.TypeOf(DefaultConfig()[provider][k])
|
|
|
|
if got != expected {
|
|
|
|
return &ConfigTypeError{
|
|
|
|
Provider: provider,
|
2022-09-20 17:43:47 +02:00
|
|
|
Key: k,
|
2022-09-20 00:54:22 +02:00
|
|
|
Expected: expected,
|
2022-09-20 17:43:47 +02:00
|
|
|
Got: got,
|
2022-09-20 00:54:22 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type ConfigTypeError struct {
|
|
|
|
Provider string
|
2022-09-20 17:43:47 +02:00
|
|
|
Key string
|
2022-09-20 00:54:22 +02:00
|
|
|
Expected reflect.Type
|
2022-09-20 17:43:47 +02:00
|
|
|
Got reflect.Type
|
2022-09-20 00:54:22 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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()
|
|
|
|
}
|
2022-09-20 17:43:47 +02:00
|
|
|
return "extractor config type error: " + e.Provider + "." + e.Key + ": expected " + expectedName + " but got " + gotName
|
2022-09-20 00:54:22 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|