Files
metatube-server/provider/tokyo-hot/tokyo-hot.go
T
2022-12-15 23:13:04 +08:00

241 lines
6.5 KiB
Go

package tokyohot
import (
"crypto/tls"
"fmt"
"net/http"
"net/url"
"path"
"regexp"
"strings"
"github.com/antchfx/htmlquery"
"github.com/gocolly/colly/v2"
"golang.org/x/net/html"
"github.com/javtube/metatube-sdk-go/common/parser"
"github.com/javtube/metatube-sdk-go/model"
"github.com/javtube/metatube-sdk-go/provider"
"github.com/javtube/metatube-sdk-go/provider/internal/scraper"
)
var (
_ provider.MovieProvider = (*TokyoHot)(nil)
_ provider.MovieSearcher = (*TokyoHot)(nil)
)
const (
Name = "TOKYO-HOT"
Priority = 1000 - 2
)
const (
baseURL = "https://my.tokyo-hot.com/"
movieURL = "https://my.tokyo-hot.com/product/%s/?lang=ja"
searchURL = "https://my.tokyo-hot.com/product/?q=%s&x=0&y=0&lang=ja"
)
type TokyoHot struct {
*scraper.Scraper
}
func New() *TokyoHot {
return &TokyoHot{scraper.NewDefaultScraper(Name, baseURL, Priority,
// Temporary workaround for unknown CA issue.
scraper.WithTransport(&http.Transport{
Proxy: http.DefaultTransport.(*http.Transport).Proxy,
DialContext: http.DefaultTransport.(*http.Transport).DialContext,
ForceAttemptHTTP2: http.DefaultTransport.(*http.Transport).ForceAttemptHTTP2,
MaxIdleConns: http.DefaultTransport.(*http.Transport).MaxIdleConns,
IdleConnTimeout: http.DefaultTransport.(*http.Transport).IdleConnTimeout,
TLSHandshakeTimeout: http.DefaultTransport.(*http.Transport).TLSHandshakeTimeout,
ExpectContinueTimeout: http.DefaultTransport.(*http.Transport).ExpectContinueTimeout,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}))}
}
func (tht *TokyoHot) NormalizeID(id string) string {
return strings.ToLower(id) /* Tokyo-Hot uses lowercase ID */
}
func (tht *TokyoHot) GetMovieInfoByID(id string) (info *model.MovieInfo, err error) {
return tht.GetMovieInfoByURL(fmt.Sprintf(movieURL, id))
}
func (tht *TokyoHot) ParseIDFromURL(rawURL string) (string, error) {
homepage, err := url.Parse(rawURL)
if err != nil {
return "", err
}
return tht.NormalizeID(path.Base(homepage.Path)), nil
}
func (tht *TokyoHot) GetMovieInfoByURL(rawURL string) (info *model.MovieInfo, err error) {
id, err := tht.ParseIDFromURL(rawURL)
if err != nil {
return
}
info = &model.MovieInfo{
ID: id,
Provider: tht.Name(),
Homepage: rawURL,
Maker: "TOKYO-HOT",
Actors: []string{},
PreviewImages: []string{},
Genres: []string{},
}
c := tht.ClonedCollector()
// Title
c.OnXML(`//*[@id="main"]//div[@class="contents"]/h2`, func(e *colly.XMLElement) {
info.Title = strings.TrimSpace(e.Text)
})
// Summary
c.OnXML(`//*[@id="main"]//div[@class="sentence"]`, func(e *colly.XMLElement) {
var sentences []string
for n := e.DOM.(*html.Node).FirstChild; n != nil; n = n.NextSibling {
if n.Type != html.TextNode {
continue
}
sentences = append(sentences, strings.TrimSpace(n.Data))
}
if len(sentences) > 0 {
info.Summary = strings.TrimSpace(strings.Join(sentences, "\n"))
}
})
// Thumb+Cover
c.OnXML(`//li[@class="package"]`, func(e *colly.XMLElement) {
for _, href := range e.ChildAttrs(`.//a`, "href") {
href = e.Request.AbsoluteURL(href)
if info.CoverURL == "" &&
(strings.HasSuffix(href, "L.jpg") ||
strings.Contains(href, "jacket")) {
info.CoverURL = href
} else if info.ThumbURL == "" &&
(strings.HasSuffix(href, "v.jpg") ||
strings.HasSuffix(href, "vb.jpg") ||
strings.Contains(href, "package")) {
info.ThumbURL = href
}
}
})
// Cover (fallback) + Video
c.OnXML(`//div[@class="flowplayer"]`, func(e *colly.XMLElement) {
if poster := e.ChildAttr(`.//video`, "poster"); info.CoverURL == "" && poster != "" {
info.CoverURL = e.Request.AbsoluteURL(poster)
}
if src := e.ChildAttr(`.//source`, "src"); src != "" {
info.PreviewVideoURL = e.Request.AbsoluteURL(src)
}
})
// Preview Images
c.OnXML(`//a[@rel="cap"]`, func(e *colly.XMLElement) {
if href := e.Attr("href"); href != "" {
info.PreviewImages = append(info.PreviewImages, e.Request.AbsoluteURL(href))
}
})
// Fields
c.OnXML(`//*[@id="main"]//div[@class="infowrapper"]/dl`, func(e *colly.XMLElement) {
for i, dt := range e.ChildTexts(`.//dt`) {
var (
dd = fmt.Sprintf(`.//dd[%d]`, i+1)
dda = fmt.Sprintf(`.//dd[%d]/a`, i+1)
)
switch dt {
case "出演者":
var actors []string
parser.ParseTexts(htmlquery.FindOne(e.DOM.(*html.Node), dd), &actors)
for _, actor := range actors {
if actor != "不明" {
info.Actors = append(info.Actors, actor)
}
}
case "プレイ内容":
parser.ParseTexts(htmlquery.FindOne(e.DOM.(*html.Node), dd),
(*[]string)(&info.Genres))
case "シリーズ":
info.Series = e.ChildText(dda)
case "レーベル":
info.Label = e.ChildText(dda)
case "配信開始日":
info.ReleaseDate = parser.ParseDate(e.ChildText(dd))
case "収録時間":
info.Runtime = parser.ParseRuntime(e.ChildText(dd))
case "作品番号":
info.Number = e.ChildText(dd)
}
}
})
// Fallbacks
c.OnScraped(func(_ *colly.Response) {
switch {
case info.ThumbURL == "" && info.CoverURL != "":
info.ThumbURL = info.CoverURL // use cover as thumb.
case info.CoverURL == "" && info.ThumbURL != "":
info.CoverURL = info.ThumbURL // vice versa.
}
})
err = c.Visit(info.Homepage)
return
}
func (tht *TokyoHot) NormalizeKeyword(keyword string) string {
if regexp.MustCompile(`^(?i)[a-z_]*\d+$`).MatchString(keyword) {
return strings.ToLower(keyword)
}
return ""
}
func (tht *TokyoHot) SearchMovie(keyword string) (results []*model.MovieSearchResult, err error) {
c := tht.ClonedCollector()
c.OnXML(`//*[@id="main"]/ul/li`, func(e *colly.XMLElement) {
img := e.Request.AbsoluteURL(e.ChildAttr(`.//a/img`, "src"))
homepage := e.Request.AbsoluteURL(e.ChildAttr(`.//a`, "href"))
id, _ := tht.ParseIDFromURL(homepage)
// id is not always the number.
var number string
if ss := regexp.MustCompile(`\(作品番号: ([a-z\d-_]+)\)`).
FindStringSubmatch(e.Text); len(ss) == 2 {
number = ss[1]
}
{ // number fallbacks
if number == "" {
number = e.ChildAttr(`.//a/img`, "alt")
}
if number == "" {
number = e.ChildAttr(`.//a/img`, "title")
}
if number == "" {
number = id
}
}
results = append(results, &model.MovieSearchResult{
ID: id,
Number: number,
Title: e.ChildText(`.//div[@class="title"]`),
ThumbURL: img,
CoverURL: img,
Provider: tht.Name(),
Homepage: homepage,
})
})
err = c.Visit(fmt.Sprintf(searchURL, url.QueryEscape(keyword)))
return
}
func init() {
provider.RegisterMovieFactory(Name, New)
}