193 lines
4.7 KiB
Go
193 lines
4.7 KiB
Go
|
package yifysubs
|
||
|
|
||
|
import (
|
||
|
"archive/zip"
|
||
|
"bytes"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"io/ioutil"
|
||
|
"net/http"
|
||
|
"path/filepath"
|
||
|
"sort"
|
||
|
"strings"
|
||
|
|
||
|
"github.com/jpillora/scraper/scraper"
|
||
|
"github.com/mitchellh/mapstructure"
|
||
|
)
|
||
|
|
||
|
// Errors
|
||
|
var (
|
||
|
ErrNoSubtitleFound = errors.New("yify: no subtitles found")
|
||
|
)
|
||
|
|
||
|
// Client represent a Client used to make Search
|
||
|
type Client struct {
|
||
|
scraper *scraper.Endpoint
|
||
|
Endpoint string
|
||
|
}
|
||
|
|
||
|
// Subtitle represents a Subtitle
|
||
|
type Subtitle struct {
|
||
|
Rating int
|
||
|
Lang string
|
||
|
Uploader string
|
||
|
URL string
|
||
|
Title string
|
||
|
reader io.ReadCloser
|
||
|
}
|
||
|
|
||
|
// New return a new Searcher
|
||
|
func New(endpoint string) *Client {
|
||
|
e := &scraper.Endpoint{
|
||
|
Name: "yifysubtitles",
|
||
|
Method: "GET",
|
||
|
List: "table.other-subs > tbody > tr",
|
||
|
Headers: map[string]string{
|
||
|
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2988.133 Safari/537.36",
|
||
|
},
|
||
|
Result: map[string]scraper.Extractors{
|
||
|
"rating": scraper.Extractors{scraper.MustExtractor("td.rating-cell"), scraper.MustExtractor("span")},
|
||
|
"lang": scraper.Extractors{scraper.MustExtractor("td.flag-cell"), scraper.MustExtractor("span.sub-lang")},
|
||
|
"title": scraper.Extractors{scraper.MustExtractor("td:nth-child(3)"), scraper.MustExtractor("a"), scraper.MustExtractor("/subtitle (.*)/")},
|
||
|
"uploader": scraper.Extractors{scraper.MustExtractor("td.uploader-cell"), scraper.MustExtractor("a")},
|
||
|
"url": scraper.Extractors{scraper.MustExtractor("td.download-cell"), scraper.MustExtractor("a"), scraper.MustExtractor("@href")},
|
||
|
},
|
||
|
Debug: false,
|
||
|
}
|
||
|
|
||
|
return &Client{
|
||
|
scraper: e,
|
||
|
Endpoint: endpoint,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Search will search Subtitles
|
||
|
func (c *Client) Search(imdbID string) ([]*Subtitle, error) {
|
||
|
c.scraper.URL = c.Endpoint + "/movie-imdb/{{imdbId}}"
|
||
|
|
||
|
vars := map[string]string{
|
||
|
"imdbId": imdbID,
|
||
|
}
|
||
|
return c.parseSubtitle(vars)
|
||
|
}
|
||
|
|
||
|
// SearchByLang will search Subtitles with given language
|
||
|
// The result will be ordered, with the highest rated subtitle first
|
||
|
func (c *Client) SearchByLang(imdbID, lang string) ([]*Subtitle, error) {
|
||
|
subtitles, err := c.Search(imdbID)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return FilterByLang(subtitles, lang), nil
|
||
|
}
|
||
|
|
||
|
// FilterByLang will filter the subtitles by language
|
||
|
// The result will be ordered, with the highest rated subtitle first
|
||
|
func FilterByLang(subtitles []*Subtitle, language string) []*Subtitle {
|
||
|
filterredSubtitles := []*Subtitle{}
|
||
|
for _, s := range subtitles {
|
||
|
if s.Lang == language {
|
||
|
filterredSubtitles = append(filterredSubtitles, s)
|
||
|
}
|
||
|
}
|
||
|
sort.Slice(filterredSubtitles, func(i, j int) bool { return filterredSubtitles[i].Rating > filterredSubtitles[j].Rating })
|
||
|
|
||
|
return filterredSubtitles
|
||
|
}
|
||
|
|
||
|
// parseSubtitle takes a map of parameters, it will do the request and return
|
||
|
// the parsed Subtitles
|
||
|
func (c *Client) parseSubtitle(vars map[string]string) ([]*Subtitle, error) {
|
||
|
// Parse the page
|
||
|
res, err := c.scraper.Execute(vars)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
subtitles := []*Subtitle{}
|
||
|
|
||
|
// Map the res to our structure
|
||
|
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
||
|
WeaklyTypedInput: true,
|
||
|
Result: &subtitles,
|
||
|
})
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
if err = decoder.Decode(res); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
if len(subtitles) == 0 {
|
||
|
return nil, ErrNoSubtitleFound
|
||
|
}
|
||
|
|
||
|
// Add the endpoint in front of the URLs
|
||
|
for _, s := range subtitles {
|
||
|
s.URL = c.Endpoint + s.URL
|
||
|
}
|
||
|
|
||
|
return subtitles, nil
|
||
|
}
|
||
|
|
||
|
// DownloadZipURL returns the zip file URL of the subtitle
|
||
|
func (s Subtitle) DownloadZipURL() string {
|
||
|
return fmt.Sprintf("%s.zip", strings.Replace(s.URL, "/subtitles/", "/subtitle/", -1))
|
||
|
}
|
||
|
|
||
|
func getReaderFromURL(url string) (io.ReadCloser, error) {
|
||
|
// Download the zip file
|
||
|
res, err := http.Get(url)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
defer res.Body.Close()
|
||
|
|
||
|
// Read all the body
|
||
|
body, err := ioutil.ReadAll(res.Body)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("Failed to read response body with error %s", err)
|
||
|
}
|
||
|
|
||
|
// Create a new zip Reader from a newly created bytes reader of the body
|
||
|
// already read
|
||
|
r, err := zip.NewReader(bytes.NewReader(body), res.ContentLength)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
for _, f := range r.File {
|
||
|
if filepath.Ext(f.Name) != ".srt" {
|
||
|
continue
|
||
|
}
|
||
|
return f.Open()
|
||
|
}
|
||
|
return nil, fmt.Errorf("Empty zip subtitle")
|
||
|
}
|
||
|
|
||
|
// Read implement the reader interface
|
||
|
func (s *Subtitle) Read(p []byte) (n int, err error) {
|
||
|
if s.reader == nil {
|
||
|
// Download the zip and get the file reader
|
||
|
r, err := getReaderFromURL(s.DownloadZipURL())
|
||
|
if err != nil {
|
||
|
return 0, err
|
||
|
}
|
||
|
s.reader = r
|
||
|
}
|
||
|
|
||
|
return s.reader.Read(p)
|
||
|
}
|
||
|
|
||
|
// Close implement the closer interface
|
||
|
func (s Subtitle) Close() error {
|
||
|
if s.reader != nil {
|
||
|
return s.reader.Close()
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|