Documentation
¶
Overview ¶
Package services provides AT Protocol integration for leaflet.pub
Document Flow:
- Pull: Fetch pub.leaflet.document records from AT Protocol repository
- Post: Create new pub.leaflet.document records in AT Protocol repository
- Push: Update existing pub.leaflet.document records in AT Protocol repository
- Delete: Remove pub.leaflet.document records from AT Protocol repository
Movies & TV: Rotten Tomatoes with colly
Music: Album of the Year with chromedp
Books: OpenLibrary API
Index ¶
- Constants
- Variables
- func AssertMovieInResults(t *testing.T, results []*models.Model, expectedTitle string)
- func AssertTVShowInResults(t *testing.T, results []*models.Model, expectedTitle string)
- func PositiveID(name string, value int64) error
- func RequiredString(name, value string) error
- func SetupFailureMocks(t *testing.T, errorMsg string) func()
- func SetupMediaMocks(t *testing.T, config MockConfig) func()
- func SetupSuccessfulMovieMocks(t *testing.T) func()
- func SetupSuccessfulTVMocks(t *testing.T) func()
- func StringLength(name, value string, min, max int) error
- func TestMovieSearch(t *testing.T, service *MovieService, query string, ...)
- func TestTVSearch(t *testing.T, service *TVService, query string, expectedTitleFragment string)
- func ValidDate(name, value string) error
- func ValidEmail(name, value string) error
- func ValidEnum(name, value string, allowedValues []string) error
- func ValidFilePath(name, value string) error
- func ValidURL(name, value string) error
- type APIService
- type ATProtoClient
- type ATProtoService
- func (s *ATProtoService) Authenticate(ctx context.Context, handle, password string) error
- func (s *ATProtoService) Close() error
- func (s *ATProtoService) DeleteDocument(ctx context.Context, rkey string, isDraft bool) error
- func (s *ATProtoService) GetDefaultPublication(ctx context.Context) (string, error)
- func (s *ATProtoService) GetSession() (*Session, error)
- func (s *ATProtoService) IsAuthenticated() bool
- func (s *ATProtoService) ListPublications(ctx context.Context) ([]PublicationWithMeta, error)
- func (s *ATProtoService) PatchDocument(ctx context.Context, rkey string, doc public.Document, isDraft bool) (*DocumentWithMeta, error)
- func (s *ATProtoService) PostDocument(ctx context.Context, doc public.Document, isDraft bool) (*DocumentWithMeta, error)
- func (s *ATProtoService) PullDocuments(ctx context.Context) ([]DocumentWithMeta, error)
- func (s *ATProtoService) RefreshToken(ctx context.Context) error
- func (s *ATProtoService) RestoreSession(session *Session) error
- func (s *ATProtoService) UploadBlob(ctx context.Context, data []byte, mimeType string) (public.Blob, error)
- type AggregateRating
- type BookService
- type DefaultFetcher
- type DocumentWithMeta
- type Fetchable
- type Media
- type MediaKind
- type MockATProtoService
- func (m *MockATProtoService) Authenticate(ctx context.Context, handle, password string) error
- func (m *MockATProtoService) Close() error
- func (m *MockATProtoService) DeleteDocument(ctx context.Context, rkey string, isDraft bool) error
- func (m *MockATProtoService) GetDefaultPublication(ctx context.Context) (string, error)
- func (m *MockATProtoService) GetSession() (*Session, error)
- func (m *MockATProtoService) IsAuthenticated() bool
- func (m *MockATProtoService) PatchDocument(ctx context.Context, rkey string, doc public.Document, isDraft bool) (*DocumentWithMeta, error)
- func (m *MockATProtoService) PostDocument(ctx context.Context, doc public.Document, isDraft bool) (*DocumentWithMeta, error)
- func (m *MockATProtoService) PullDocuments(ctx context.Context) ([]DocumentWithMeta, error)
- func (m *MockATProtoService) RestoreSession(session *Session) error
- func (m *MockATProtoService) UploadBlob(ctx context.Context, data []byte, mimeType string) (public.Blob, error)
- type MockConfig
- type MockSetup
- type Movie
- type MovieService
- type MutateRecordOutput
- type OpenLibraryAuthorKey
- type OpenLibraryAuthorRef
- type OpenLibrarySearchDoc
- type OpenLibrarySearchResponse
- type OpenLibraryType
- type OpenLibraryWork
- type PartOfSeries
- type Person
- type PublicationWithMeta
- type Searchable
- type Season
- type Session
- type TVSeason
- type TVSeries
- type TVService
- type ValidationError
- type ValidationErrors
- type Validator
Constants ¶
const ( // Open Library API endpoints OpenLibraryBaseURL string = "https://openlibrary.org" )
Variables ¶
var ExtractMovieMetadata = func(r io.Reader) (*Movie, error) { doc, err := goquery.NewDocumentFromReader(r) if err != nil { return nil, err } var movie Movie found := false doc.Find("script[type='application/ld+json']").Each(func(i int, s *goquery.Selection) { var tmp map[string]any if err := json.Unmarshal([]byte(s.Text()), &tmp); err == nil { if t, ok := tmp["@type"].(string); ok && t == "Movie" { if err := json.Unmarshal([]byte(s.Text()), &movie); err == nil { found = true } } } }) if !found { return nil, fmt.Errorf("no Movie JSON-LD found") } return &movie, nil }
var ExtractTVSeasonMetadata = func(r io.Reader) (*TVSeason, error) { doc, err := goquery.NewDocumentFromReader(r) if err != nil { return nil, err } var season TVSeason found := false doc.Find("script[type='application/ld+json']").Each(func(i int, s *goquery.Selection) { var tmp map[string]any if err := json.Unmarshal([]byte(s.Text()), &tmp); err == nil { if t, ok := tmp["@type"].(string); ok && t == "TVSeason" { if err := json.Unmarshal([]byte(s.Text()), &season); err == nil { found = true } } } }) if !found { return nil, fmt.Errorf("no TVSeason JSON-LD found") } if season.SeasonNumber == 0 { if season.URL != "" { parts := strings.SplitSeq(season.URL, "/") for part := range parts { if strings.HasPrefix(part, "s") && len(part) > 1 { if num, err := strconv.Atoi(part[1:]); err == nil { season.SeasonNumber = num break } } } } if season.SeasonNumber == 0 && season.Name != "" { parts := strings.Fields(season.Name) for i, part := range parts { if strings.ToLower(part) == "season" && i+1 < len(parts) { if num, err := strconv.Atoi(parts[i+1]); err == nil { season.SeasonNumber = num break } } } } } return &season, nil }
var ExtractTVSeriesMetadata = func(r io.Reader) (*TVSeries, error) { doc, err := goquery.NewDocumentFromReader(r) if err != nil { return nil, err } var series TVSeries found := false doc.Find("script[type='application/ld+json']").Each(func(i int, s *goquery.Selection) { var tmp map[string]any if err := json.Unmarshal([]byte(s.Text()), &tmp); err == nil { if t, ok := tmp["@type"].(string); ok && t == "TVSeries" { if err := json.Unmarshal([]byte(s.Text()), &series); err == nil { found = true } } } }) if !found { return nil, fmt.Errorf("no TVSeries JSON-LD found") } return &series, nil }
var FetchHTML = func(url string) (string, error) { var html string c := colly.NewCollector( colly.AllowedDomains("www.rottentomatoes.com", "rottentomatoes.com"), ) c.OnResponse(func(r *colly.Response) { html = string(r.Body) }) if err := c.Visit(url); err != nil { return "", err } return html, nil }
var FetchMovie = func(url string) (*Movie, error) { html, err := FetchHTML(url) if err != nil { return nil, err } return ExtractMovieMetadata(strings.NewReader(html)) }
var FetchTVSeason = func(url string) (*TVSeason, error) { html, err := FetchHTML(url) if err != nil { return nil, err } return ExtractTVSeasonMetadata(strings.NewReader(html)) }
var FetchTVSeries = func(url string) (*TVSeries, error) { html, err := FetchHTML(url) if err != nil { return nil, err } return ExtractTVSeriesMetadata(strings.NewReader(html)) }
var MovieSample []byte
From: https://www.rottentomatoes.com/m/the_fantastic_four_first_steps
var MovieSearchSample []byte
From: https://www.rottentomatoes.com/search?search=Fantastic%20Four
var ParseSearch = func(r io.Reader) ([]Media, error) { doc, err := goquery.NewDocumentFromReader(r) if err != nil { return nil, err } var results []Media doc.Find("search-page-result").Each(func(i int, resultBlock *goquery.Selection) { mediaType, _ := resultBlock.Attr("type") resultBlock.Find("search-page-media-row").Each(func(j int, s *goquery.Selection) { link, _ := s.Find("a[slot='thumbnail']").Attr("href") if link == "" { link, _ = s.Find("a[slot='title']").Attr("href") if link == "" { return } } title := s.Find("a[slot='title']").Text() var itemKind MediaKind switch mediaType { case "movie": itemKind = MovieKind case "tvSeries": itemKind = TVKind default: if strings.HasPrefix(link, "/m/") { itemKind = MovieKind } else if strings.HasPrefix(link, "/tv/") { itemKind = TVKind } } score, _ := s.Attr("tomatometerscore") if score == "" { score = "--" } certified := false if v, ok := s.Attr("tomatometeriscertified"); ok && v == "true" { certified = true } results = append(results, Media{ Title: strings.TrimSpace(title), Link: link, Type: itemKind, CriticScore: score, CertifiedFresh: certified, }) }) }) return results, nil }
ParseSearch parses Rotten Tomatoes search results HTML into Media entries.
var SearchRottenTomatoes = func(q string) ([]Media, error) { searchURL := "https://www.rottentomatoes.com/search?search=" + url.QueryEscape(q) html, err := FetchHTML(searchURL) if err != nil { return nil, err } return ParseSearch(strings.NewReader(html)) }
SearchRottenTomatoes fetches live search results for a query.
var SearchSample []byte
From: https://www.rottentomatoes.com/search?search=peacemaker
var SeasonSample []byte
From: https://www.rottentomatoes.com/tv/peacemaker_2022/s02
var SeriesSample []byte
Functions ¶
func AssertMovieInResults ¶
AssertMovieInResults checks if a movie with the given title exists in results
func AssertTVShowInResults ¶
AssertTVShowInResults checks if a TV show with the given title exists in results
func PositiveID ¶
PositiveID validates that an ID is positive
func RequiredString ¶
RequiredString validates that a string field is not empty
func SetupFailureMocks ¶
SetupFailureMocks configures mocks that return errors
func SetupMediaMocks ¶
func SetupMediaMocks(t *testing.T, config MockConfig) func()
SetupMediaMocks configures mock functions for media services testing
func SetupSuccessfulMovieMocks ¶
SetupSuccessfulMovieMocks configures mocks for successful movie operations
func SetupSuccessfulTVMocks ¶
SetupSuccessfulTVMocks configures mocks for successful TV operations
func StringLength ¶
StringLength validates string length constraints
func TestMovieSearch ¶
func TestMovieSearch(t *testing.T, service *MovieService, query string, expectedTitleFragment string)
TestMovieSearch runs a standard movie search test
func TestTVSearch ¶
TestTVSearch runs a standard TV search test
func ValidEmail ¶
ValidEmail validates that a string is a valid email address
func ValidFilePath ¶
ValidFilePath validates that a string looks like a valid file path
Types ¶
type APIService ¶
type APIService interface {
Get(ctx context.Context, id string) (*models.Model, error)
Search(ctx context.Context, query string, page, limit int) ([]*models.Model, error)
Check(ctx context.Context) error
Close() error
}
APIService defines the contract for API interactions
type ATProtoClient ¶
type ATProtoClient interface {
Authenticate(ctx context.Context, handle, password string) error
GetSession() (*Session, error)
IsAuthenticated() bool
RestoreSession(session *Session) error
PullDocuments(ctx context.Context) ([]DocumentWithMeta, error)
PostDocument(ctx context.Context, doc public.Document, isDraft bool) (*DocumentWithMeta, error)
PatchDocument(ctx context.Context, rkey string, doc public.Document, isDraft bool) (*DocumentWithMeta, error)
DeleteDocument(ctx context.Context, rkey string, isDraft bool) error
UploadBlob(ctx context.Context, data []byte, mimeType string) (public.Blob, error)
GetDefaultPublication(ctx context.Context) (string, error)
Close() error
}
ATProtoClient defines the interface for AT Protocol operations
type ATProtoService ¶
type ATProtoService struct {
// contains filtered or unexported fields
}
ATProtoService provides AT Protocol operations for leaflet integration
func NewATProtoService ¶
func NewATProtoService() *ATProtoService
NewATProtoService creates a new AT Protocol service
func (*ATProtoService) Authenticate ¶
func (s *ATProtoService) Authenticate(ctx context.Context, handle, password string) error
Authenticate logs in with BlueSky/AT Protocol credentials
func (*ATProtoService) DeleteDocument ¶
DeleteDocument removes a document from the user's repository
func (*ATProtoService) GetDefaultPublication ¶
func (s *ATProtoService) GetDefaultPublication(ctx context.Context) (string, error)
GetDefaultPublication returns the URI of the first available publication for the authenticated user
Returns an error if no publications exist
func (*ATProtoService) GetSession ¶
func (s *ATProtoService) GetSession() (*Session, error)
GetSession returns the current session information
func (*ATProtoService) IsAuthenticated ¶
func (s *ATProtoService) IsAuthenticated() bool
IsAuthenticated checks if the service has a valid session
func (*ATProtoService) ListPublications ¶
func (s *ATProtoService) ListPublications(ctx context.Context) ([]PublicationWithMeta, error)
ListPublications fetches available publications for the authenticated user
func (*ATProtoService) PatchDocument ¶
func (s *ATProtoService) PatchDocument(ctx context.Context, rkey string, doc public.Document, isDraft bool) (*DocumentWithMeta, error)
PatchDocument updates an existing document in the user's repository
func (*ATProtoService) PostDocument ¶
func (s *ATProtoService) PostDocument(ctx context.Context, doc public.Document, isDraft bool) (*DocumentWithMeta, error)
PostDocument creates a new document in the user's repository
func (*ATProtoService) PullDocuments ¶
func (s *ATProtoService) PullDocuments(ctx context.Context) ([]DocumentWithMeta, error)
PullDocuments fetches all leaflet documents from the user's repository
func (*ATProtoService) RefreshToken ¶
func (s *ATProtoService) RefreshToken(ctx context.Context) error
RefreshToken refreshes the access token using the refresh token This extends the session without requiring the user to re-authenticate
func (*ATProtoService) RestoreSession ¶
func (s *ATProtoService) RestoreSession(session *Session) error
RestoreSession restores a previously authenticated session from stored credentials and automatically refreshes the token if expired
func (*ATProtoService) UploadBlob ¶
func (s *ATProtoService) UploadBlob(ctx context.Context, data []byte, mimeType string) (public.Blob, error)
UploadBlob uploads binary data as a blob to AT Protocol
type AggregateRating ¶
type BookService ¶
type BookService struct {
// contains filtered or unexported fields
}
BookService implements APIService for Open Library
func NewBookService ¶
func NewBookService(baseURL string) *BookService
NewBookService creates a new book service with rate limiting
func (*BookService) Check ¶
func (bs *BookService) Check(ctx context.Context) error
Check verifies the API connection
func (*BookService) Close ¶
func (bs *BookService) Close() error
Close cleans up the service resources
HTTP client doesn't need explicit cleanup
type DefaultFetcher ¶
type DefaultFetcher struct{}
DefaultFetcher provides the default implementation using colly
func (*DefaultFetcher) MakeRequest ¶
func (f *DefaultFetcher) MakeRequest(url string) (string, error)
func (*DefaultFetcher) MovieRequest ¶
func (f *DefaultFetcher) MovieRequest(url string) (*Movie, error)
type DocumentWithMeta ¶
type DocumentWithMeta struct {
Document public.Document
Meta public.DocumentMeta
}
DocumentWithMeta combines a document with its repository metadata
type Media ¶
type Media struct {
Title string
Link string
Type MediaKind
CriticScore string
CertifiedFresh bool
}
func GetSampleMovieSearchResults ¶
Sample data access helpers - these use the embedded samples
func GetSampleSearchResults ¶
type MockATProtoService ¶
type MockATProtoService struct {
AuthenticateFunc func(ctx context.Context, handle, password string) error
GetSessionFunc func() (*Session, error)
IsAuthenticatedVal bool
RestoreSessionFunc func(session *Session) error
PullDocumentsFunc func(ctx context.Context) ([]DocumentWithMeta, error)
PostDocumentFunc func(ctx context.Context, doc public.Document, isDraft bool) (*DocumentWithMeta, error)
PatchDocumentFunc func(ctx context.Context, rkey string, doc public.Document, isDraft bool) (*DocumentWithMeta, error)
DeleteDocumentFunc func(ctx context.Context, rkey string, isDraft bool) error
UploadBlobFunc func(ctx context.Context, data []byte, mimeType string) (public.Blob, error)
GetDefaultPublicationFunc func(ctx context.Context) (string, error)
CloseFunc func() error
Session *Session // Exported for test access
}
MockATProtoService is a mock implementation of ATProtoService for testing
func NewMockATProtoService ¶
func NewMockATProtoService() *MockATProtoService
NewMockATProtoService creates a new mock AT Proto service
func SetupSuccessfulAuthMocks ¶
func SetupSuccessfulAuthMocks() *MockATProtoService
SetupSuccessfulAuthMocks configures mock for successful authentication
func SetupSuccessfulPullMocks ¶
func SetupSuccessfulPullMocks() *MockATProtoService
SetupSuccessfulPullMocks configures mock for successful document pull
func (*MockATProtoService) Authenticate ¶
func (m *MockATProtoService) Authenticate(ctx context.Context, handle, password string) error
Authenticate mocks authentication
func (*MockATProtoService) DeleteDocument ¶
DeleteDocument mocks deleting a document
func (*MockATProtoService) GetDefaultPublication ¶
func (m *MockATProtoService) GetDefaultPublication(ctx context.Context) (string, error)
GetDefaultPublication mocks getting the default publication
func (*MockATProtoService) GetSession ¶
func (m *MockATProtoService) GetSession() (*Session, error)
GetSession returns the current session
func (*MockATProtoService) IsAuthenticated ¶
func (m *MockATProtoService) IsAuthenticated() bool
IsAuthenticated returns authentication status
func (*MockATProtoService) PatchDocument ¶
func (m *MockATProtoService) PatchDocument(ctx context.Context, rkey string, doc public.Document, isDraft bool) (*DocumentWithMeta, error)
PatchDocument mocks patching a document
func (*MockATProtoService) PostDocument ¶
func (m *MockATProtoService) PostDocument(ctx context.Context, doc public.Document, isDraft bool) (*DocumentWithMeta, error)
PostDocument mocks posting a document
func (*MockATProtoService) PullDocuments ¶
func (m *MockATProtoService) PullDocuments(ctx context.Context) ([]DocumentWithMeta, error)
PullDocuments mocks pulling documents
func (*MockATProtoService) RestoreSession ¶
func (m *MockATProtoService) RestoreSession(session *Session) error
RestoreSession restores a session
func (*MockATProtoService) UploadBlob ¶
func (m *MockATProtoService) UploadBlob(ctx context.Context, data []byte, mimeType string) (public.Blob, error)
UploadBlob mocks blob upload
type MockConfig ¶
type MockConfig struct {
SearchResults []Media
SearchError error
MovieResult *Movie
MovieError error
TVSeriesResult *TVSeries
TVSeriesError error
TVSeasonResult *TVSeason
TVSeasonError error
HTMLResult string
HTMLError error
}
MockConfig holds configuration for mocking media services
type MockSetup ¶
type MockSetup struct {
// contains filtered or unexported fields
}
MockSetup contains the original function variables for restoration
type Movie ¶
type Movie struct {
Context string `json:"@context"`
Type string `json:"@type"`
Name string `json:"name"`
URL string `json:"url"`
Description string `json:"description"`
Image string `json:"image"`
Genre []string `json:"genre"`
ContentRating string `json:"contentRating"`
DateCreated string `json:"dateCreated"`
Actors []Person `json:"actor"`
Directors []Person `json:"director"`
Producers []Person `json:"producer"`
AggregateRating AggregateRating `json:"aggregateRating"`
}
func GetSampleMovie ¶
type MovieService ¶
type MovieService struct {
// contains filtered or unexported fields
}
func CreateMovieService ¶
func CreateMovieService() *MovieService
CreateMovieService returns a new movie service for testing
func NewMovieService ¶
func NewMovieService() *MovieService
NewMovieService creates a new movie service with rate limiting
func NewMovieSrvWithOpts ¶
func NewMovieSrvWithOpts(baseURL string, fetcher Fetchable, searcher Searchable) *MovieService
NewMovieSrvWithOpts creates a new movie service with custom dependencies (for testing)
func (*MovieService) Check ¶
func (s *MovieService) Check(ctx context.Context) error
Check verifies the API connection to Rotten Tomatoes
func (*MovieService) Close ¶
func (s *MovieService) Close() error
Close cleans up the service resources
type MutateRecordOutput ¶
type OpenLibraryAuthorKey ¶
type OpenLibraryAuthorKey struct {
Key string `json:"key"`
}
OpenLibraryAuthorKey represents an author key
type OpenLibraryAuthorRef ¶
type OpenLibraryAuthorRef struct {
Author OpenLibraryAuthorKey `json:"author"`
Type OpenLibraryType `json:"type"`
}
OpenLibraryAuthorRef represents an author reference in a work
type OpenLibrarySearchDoc ¶
type OpenLibrarySearchDoc struct {
Key string `json:"key"`
Title string `json:"title"`
AuthorName []string `json:"author_name"`
FirstPublishYear int `json:"first_publish_year"`
PublishYear []int `json:"publish_year"`
Edition_count int `json:"edition_count"`
ISBN []string `json:"isbn"`
PublisherName []string `json:"publisher"`
Subject []string `json:"subject"`
CoverI int `json:"cover_i"`
HasFulltext bool `json:"has_fulltext"`
PublicScanB bool `json:"public_scan_b"`
ReadinglogCount int `json:"readinglog_count"`
WantToReadCount int `json:"want_to_read_count"`
CurrentlyReading int `json:"currently_reading_count"`
AlreadyReadCount int `json:"already_read_count"`
}
OpenLibrarySearchDoc represents a book document in search results
type OpenLibrarySearchResponse ¶
type OpenLibrarySearchResponse struct {
NumFound int `json:"numFound"`
Start int `json:"start"`
NumFoundExact bool `json:"numFoundExact"`
Docs []OpenLibrarySearchDoc `json:"docs"`
}
OpenLibrarySearchResponse represents the search response from Open Library
type OpenLibraryType ¶
type OpenLibraryType struct {
Key string `json:"key"`
}
OpenLibraryType represents a type reference
type OpenLibraryWork ¶
type OpenLibraryWork struct {
Key string `json:"key"`
Title string `json:"title"`
Authors []OpenLibraryAuthorRef `json:"authors"`
Description any `json:"description"` // Can be string or object
Subjects []string `json:"subjects"`
Covers []int `json:"covers"`
FirstPublishDate string `json:"first_publish_date"`
}
OpenLibraryWork represents a work details from Open Library
type PartOfSeries ¶
type PublicationWithMeta ¶
type PublicationWithMeta struct {
Publication public.Publication
RKey string
CID string
URI string
}
PublicationWithMeta combines a publication with its metadata
type Searchable ¶
type Session ¶
type Session struct {
DID string // Decentralized Identifier
Handle string // User handle (e.g., username.bsky.social)
AccessJWT string // Access token
RefreshJWT string // Refresh token
PDSURL string // Personal Data Server URL
ExpiresAt time.Time // When access token expires
Authenticated bool // Whether session is valid
}
Session holds authentication session information
type TVSeason ¶
type TVSeason struct {
Context string `json:"@context"`
Type string `json:"@type"`
Name string `json:"name"`
URL string `json:"url"`
Description string `json:"description"`
Image string `json:"image"`
SeasonNumber int `json:"seasonNumber"`
DatePublished string `json:"datePublished"`
PartOfSeries PartOfSeries `json:"partOfSeries"`
AggregateRating AggregateRating `json:"aggregateRating"`
}
func GetSampleTVSeason ¶
type TVSeries ¶
type TVSeries struct {
Context string `json:"@context"`
Type string `json:"@type"`
Name string `json:"name"`
URL string `json:"url"`
Description string `json:"description"`
Image string `json:"image"`
Genre []string `json:"genre"`
ContentRating string `json:"contentRating"`
DateCreated string `json:"dateCreated"`
NumberOfSeasons int `json:"numberOfSeasons"`
Actors []Person `json:"actor"`
Producers []Person `json:"producer"`
AggregateRating AggregateRating `json:"aggregateRating"`
Seasons []Season `json:"containsSeason"`
}
func GetSampleTVSeries ¶
type TVService ¶
type TVService struct {
// contains filtered or unexported fields
}
func CreateTVService ¶
func CreateTVService() *TVService
CreateTVService returns a new TV service for testing
func NewTVService ¶
func NewTVService() *TVService
NewTVService creates a new TV service with rate limiting
func NewTVServiceWithDeps ¶
func NewTVServiceWithDeps(baseURL string, fetcher Fetchable, searcher Searchable) *TVService
NewTVServiceWithDeps creates a new TV service with custom dependencies (for testing)
type ValidationError ¶
ValidationError represents a validation error
func NewValidationError ¶
func NewValidationError(f, m string) ValidationError
func (ValidationError) Error ¶
func (e ValidationError) Error() string
type ValidationErrors ¶
type ValidationErrors []ValidationError
ValidationErrors represents multiple validation errors
func (ValidationErrors) Error ¶
func (e ValidationErrors) Error() string