init
This commit is contained in:
208
extractor/extractor.go
Normal file
208
extractor/extractor.go
Normal file
@@ -0,0 +1,208 @@
|
||||
package extractor
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoSearchResults = errors.New("no search provider available")
|
||||
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.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,
|
||||
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 "extractor config type error: "+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
|
||||
}
|
||||
Reference in New Issue
Block a user