From 3200ddc473b230a5fc523fc0cca848684dae9eed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Delattre?= Date: Thu, 21 Nov 2024 11:04:36 +0100 Subject: [PATCH] Add a bsplayer module to get subtitles --- app/polochon_modules.go | 1 + config.example.yml | 1 + modules/bsplayer/bsplayer.go | 230 +++++++++++++++++++++++++++++++++++ modules/bsplayer/module.go | 111 +++++++++++++++++ 4 files changed, 343 insertions(+) create mode 100644 modules/bsplayer/bsplayer.go create mode 100644 modules/bsplayer/module.go diff --git a/app/polochon_modules.go b/app/polochon_modules.go index dc45b41..1cc5610 100644 --- a/app/polochon_modules.go +++ b/app/polochon_modules.go @@ -4,6 +4,7 @@ import ( // Modules _ "github.com/odwrtw/polochon/modules/addicted" _ "github.com/odwrtw/polochon/modules/aria2" + _ "github.com/odwrtw/polochon/modules/bsplayer" _ "github.com/odwrtw/polochon/modules/canape" _ "github.com/odwrtw/polochon/modules/eztv" _ "github.com/odwrtw/polochon/modules/fsnotify" diff --git a/config.example.yml b/config.example.yml index fbe27e1..593f370 100644 --- a/config.example.yml +++ b/config.example.yml @@ -140,6 +140,7 @@ movie: - mkvinfo - yifysubs - opensubtitles + - bsplayer modules_params: # Required for the transmission client, if the downloader is enabled. diff --git a/modules/bsplayer/bsplayer.go b/modules/bsplayer/bsplayer.go new file mode 100644 index 0000000..4c873f5 --- /dev/null +++ b/modules/bsplayer/bsplayer.go @@ -0,0 +1,230 @@ +package bsplayer + +import ( + "bytes" + "compress/gzip" + "encoding/xml" + "fmt" + "io" + "math/rand" + "net/http" + "strconv" + "strings" + "text/template" + "time" + + polochon "github.com/odwrtw/polochon/lib" +) + +var ( + random *rand.Rand + baseURL = "http://s%d.api.bsplayer-subtitles.com/v1.php" + domains = []int{1, 2, 3, 4, 5, 6, 7, 8, 101, 102, 103, 104, 105, 106, 107, 108, 109} + soapTemplate = ` + + + {{.Params}} + + +` + soap = template.Must(template.New("soap").Parse(soapTemplate)) + loginPayload = ` + + + BSPlayer v2.72 + ` +) + +func init() { + random = rand.New(rand.NewSource(time.Now().UnixNano())) +} + +type soapParams struct { + Endpoint string + Action string + Params string +} + +type loginResponse struct { + Status int `xml:"Body>logInResponse>return>result"` + Token string `xml:"Body>logInResponse>return>data"` +} + +type subtitle struct { + Lang string `xml:"subLang"` + Name string `xml:"subName"` + Format string `xml:"subFormat"` + URL string `xml:"subDownloadLink"` + Rating string `xml:"subRating"` + + FileHash string `xml:"movieHash"` + FileSize string `xml:"movieSize"` + ImdbID string `xml:"movieIMBDID"` +} + +func (s *subtitle) String() string { + var out strings.Builder + fmt.Fprintf(&out, "Name:%s\n", s.Name) + fmt.Fprintf(&out, "ImdbID:%s\n", s.ImdbID) + fmt.Fprintf(&out, "Lang:%s\n", s.Lang) + fmt.Fprintf(&out, "Format:%s\n", s.Format) + fmt.Fprintf(&out, "Rating:%s\n", s.Rating) + fmt.Fprintf(&out, "Hash:%s\n", s.FileHash) + fmt.Fprintf(&out, "Size:%s\n", s.FileSize) + fmt.Fprintf(&out, "URL:%s\n", s.URL) + return out.String() +} + +type searchResponse struct { + Status int `xml:"Body>searchSubtitlesResponse>return>result>result"` + Subs []*subtitle `xml:"Body>searchSubtitlesResponse>return>data>item"` +} + +func getEndpoint() string { + domain := domains[random.Intn(len(domains))] + return fmt.Sprintf(baseURL, domain) +} + +func query(endpoint, action, payload string) ([]byte, error) { + params := &bytes.Buffer{} + err := soap.Execute(params, &soapParams{ + Endpoint: endpoint, + Action: action, + Params: payload, + }) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", endpoint, params) + if err != nil { + return nil, err + } + req.Header.Add("User-Agent", "BSPlayer/2.x (1022.12362)") + req.Header.Add("Content-Type", "text/xml; charset=utf-8") + req.Header.Add("Connection", "close") + req.Header.Add("SoapAction", fmt.Sprintf("%s#%s", endpoint, action)) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("bsplayer: http response code %d", resp.StatusCode) + } + + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + return data, nil +} + +func login() (string, string, error) { + endpoint := getEndpoint() + + body, err := query(endpoint, "logIn", loginPayload) + if err != nil { + return "", "", err + } + + data := loginResponse{} + if err := xml.Unmarshal(body, &data); err != nil { + return "", "", err + } + + if data.Status != http.StatusOK { + return "", "", fmt.Errorf("bsplayer: xml login response code %d", data.Status) + } + + if data.Token == "" { + return "", "", fmt.Errorf("bsplayer: missing login token") + } + + return endpoint, data.Token, nil +} + +type queryParams struct { + imdbID string + size string + hash string + lang string +} + +var langs = map[polochon.Language]string{ + polochon.EN: "eng", + polochon.FR: "fre", +} + +func newQuery(imdbID string, lang polochon.Language, file *polochon.File) (*queryParams, error) { + if file == nil { + return nil, fmt.Errorf("bsplayer: missing file") + } + + l, ok := langs[lang] + if !ok { + return nil, fmt.Errorf("bsplayer: lang %s not handled", lang) + } + + hash, err := file.OpensubHash() + if err != nil { + return nil, err + } + + return &queryParams{ + imdbID: strings.ReplaceAll(imdbID, "tt", ""), + hash: fmt.Sprintf("%016x", hash), + size: strconv.Itoa(int(file.Size)), + lang: l, + }, nil +} + +func search(qp *queryParams) ([]*subtitle, error) { + endpoint, token, err := login() + if err != nil { + return nil, err + } + + params := strings.Builder{} + fmt.Fprintf(¶ms, "%s", token) + fmt.Fprintf(¶ms, "%s", qp.hash) + fmt.Fprintf(¶ms, "%s", qp.size) + fmt.Fprintf(¶ms, "%s", qp.lang) + fmt.Fprintf(¶ms, "%s", qp.imdbID) + + ret, err := query(endpoint, "searchSubtitles", params.String()) + if err != nil { + return nil, err + } + + data := searchResponse{} + if err := xml.Unmarshal(ret, &data); err != nil { + return nil, err + } + + if data.Status != http.StatusOK { + return nil, fmt.Errorf("bsplayer: login response code %d", data.Status) + } + + return data.Subs, nil +} + +func fetch(url string) (io.ReadCloser, error) { + resp, err := http.Get(url) + if err != nil { + return nil, err + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("bsplayer: fetch http status: %s", resp.Status) + } + + return gzip.NewReader(resp.Body) +} diff --git a/modules/bsplayer/module.go b/modules/bsplayer/module.go new file mode 100644 index 0000000..c9c6c2a --- /dev/null +++ b/modules/bsplayer/module.go @@ -0,0 +1,111 @@ +package bsplayer + +import ( + "errors" + "io" + "path/filepath" + + "github.com/agnivade/levenshtein" + polochon "github.com/odwrtw/polochon/lib" + "github.com/sirupsen/logrus" +) + +const moduleName = "bsplayer" + +var _ polochon.Subtitler = (*Client)(nil) + +var ErrNotAVideo = errors.New("bsplayer: not a video") + +func init() { + polochon.RegisterModule(&Client{}) +} + +// Client represents the bsplayer API client. +type Client struct { +} + +// Init implements the polochon.Module interface. +func (c *Client) Init(_ []byte) error { + return nil +} + +// Name implements the polochon.Module interface. +func (c *Client) Name() string { + return moduleName +} + +// Status implements the polochon.Module interface. +func (c *Client) Status() (polochon.ModuleStatus, error) { + qp := &queryParams{ + imdbID: "133093", + size: "1991716652", + hash: "6513e3c7b21e645c", + lang: "eng", + } + + subs, err := search(qp) + if err != nil || len(subs) == 0 { + return polochon.StatusFail, err + } + + return polochon.StatusOK, nil +} + +// GetSubtitle implements the polochon.Subtitler interface. +func (c *Client) GetSubtitle(i interface{}, lang polochon.Language, _ *logrus.Entry) (*polochon.Subtitle, error) { + var qp *queryParams + var err error + + switch resource := i.(type) { + case *polochon.Movie: + qp, err = newQuery(resource.ImdbID, lang, resource.GetFile()) + case *polochon.ShowEpisode: + qp, err = newQuery(resource.ShowImdbID, lang, resource.GetFile()) + default: + return nil, ErrNotAVideo + } + + if err != nil { + return nil, err + } + + subs, err := search(qp) + if err != nil { + return nil, err + } + + video, ok := i.(polochon.Video) + if !ok { + return nil, ErrNotAVideo + } + + var selected *subtitle + minScore := 1000 + + release := filepath.Base(video.GetFile().PathWithoutExt()) + for _, sub := range subs { + dist := levenshtein.ComputeDistance(release, sub.Name) + if dist < minScore { + selected = sub + minScore = dist + } + } + + if selected == nil { + return nil, polochon.ErrNoSubtitleFound + } + + rc, err := fetch(selected.URL) + if err != nil { + return nil, err + } + defer rc.Close() + + s := polochon.NewSubtitleFromVideo(video, lang) + s.Data, err = io.ReadAll(rc) + if err != nil { + return nil, err + } + + return s, nil +}