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

226 lines
6.1 KiB
Go

package heyzo
import (
"bytes"
"encoding/json"
"fmt"
"net/url"
"path"
"regexp"
"strings"
"github.com/gocolly/colly/v2"
"github.com/javtube/metatube-sdk-go/common/m3u8"
"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 = (*Heyzo)(nil)
const (
Name = "HEYZO"
Priority = 1000
)
const (
baseURL = "https://www.heyzo.com/"
movieURL = "https://www.heyzo.com/moviepages/%04s/index.html"
sampleURL = "https://www.heyzo.com/contents/%s/%s/%s"
)
type Heyzo struct {
*scraper.Scraper
}
func New() *Heyzo {
return &Heyzo{scraper.NewDefaultScraper(Name, baseURL, Priority)}
}
func (hzo *Heyzo) NormalizeID(id string) string {
if ss := regexp.MustCompile(`^(?i)(?:heyzo[-_])?(\d+)$`).FindStringSubmatch(id); len(ss) == 2 {
return ss[1]
}
return ""
}
func (hzo *Heyzo) GetMovieInfoByID(id string) (info *model.MovieInfo, err error) {
return hzo.GetMovieInfoByURL(fmt.Sprintf(movieURL, id))
}
func (hzo *Heyzo) ParseIDFromURL(rawURL string) (string, error) {
homepage, err := url.Parse(rawURL)
if err != nil {
return "", err
}
return path.Base(path.Dir(homepage.Path)), nil
}
func (hzo *Heyzo) GetMovieInfoByURL(rawURL string) (info *model.MovieInfo, err error) {
id, err := hzo.ParseIDFromURL(rawURL)
if err != nil {
return
}
info = &model.MovieInfo{
ID: id,
Number: fmt.Sprintf("HEYZO-%s", id),
Provider: hzo.Name(),
Homepage: rawURL,
Maker: "HEYZO",
Actors: []string{},
PreviewImages: []string{},
Genres: []string{},
}
c := hzo.ClonedCollector()
// JSON
c.OnXML(`//script[@type="application/ld+json"]`, func(e *colly.XMLElement) {
data := struct {
Name string `json:"name"`
Image string `json:"image"`
Description string `json:"description"`
ReleasedEvent struct {
StartDate string `json:"startDate"`
} `json:"releasedEvent"`
Video struct {
Duration string `json:"duration"`
Actor string `json:"actor"`
Provider string `json:"provider"`
} `json:"video"`
AggregateRating struct {
RatingValue string `json:"ratingValue"`
} `json:"aggregateRating"`
}{}
if json.Unmarshal([]byte(e.Text), &data) == nil {
info.Title = data.Name
info.Summary = data.Description
info.CoverURL = e.Request.AbsoluteURL(data.Image)
info.ThumbURL = info.CoverURL /* use cover as thumb */
info.ReleaseDate = parser.ParseDate(data.ReleasedEvent.StartDate)
info.Runtime = parser.ParseRuntime(data.Video.Duration)
info.Score = parser.ParseScore(data.AggregateRating.RatingValue)
if data.Video.Provider != "" {
info.Maker = data.Video.Provider
}
if data.Video.Actor != "" {
info.Actors = []string{data.Video.Actor}
}
}
})
// Title
c.OnXML(`//*[@id="movie"]/h1`, func(e *colly.XMLElement) {
if info.Title == "" {
info.Title = strings.Fields(e.Text)[0]
}
})
// Summary
c.OnXML(`//p[@class="memo"]`, func(e *colly.XMLElement) {
if info.Summary == "" {
info.Summary = strings.TrimSpace(e.Text)
}
})
// Thumb+Cover (fallback)
c.OnXML(`//meta[@property="og:image"]`, func(e *colly.XMLElement) {
if info.CoverURL == "" {
info.CoverURL = e.Request.AbsoluteURL(e.Attr("content"))
info.ThumbURL = info.CoverURL
}
})
// Fields
c.OnXML(`//table[@class="movieInfo"]/tbody/tr`, func(e *colly.XMLElement) {
switch e.ChildText(`.//td[1]`) {
case "公開日":
info.ReleaseDate = parser.ParseDate(e.ChildText(`.//td[2]`))
case "出演":
info.Actors = e.ChildTexts(`.//td[2]/a/span`)
case "シリーズ":
info.Series = strings.Trim(e.ChildText(`.//td[2]`), "-")
case "評価":
info.Score = parser.ParseScore(e.ChildText(`.//span[@itemprop="ratingValue"]`))
}
})
// Genres
c.OnXML(`//ul[@class="tag-keyword-list"]`, func(e *colly.XMLElement) {
info.Genres = e.ChildTexts(`.//li/a`)
})
// Video+Runtime
c.OnXML(`//script[@type="text/javascript"]`, func(e *colly.XMLElement) {
// Sample Video
if strings.Contains(e.Text, "emvideo") {
if sub := regexp.MustCompile(`emvideo = "(.+?)";`).FindStringSubmatch(e.Text); len(sub) == 2 {
info.PreviewVideoURL = e.Request.AbsoluteURL(sub[1])
}
}
// Runtime
if strings.Contains(e.Text, "o = {") {
if sub := regexp.MustCompile(`o = (\{.+?});`).FindStringSubmatch(e.Text); len(sub) == 2 {
data := struct {
Full string `json:"full"`
}{}
if json.Unmarshal([]byte(sub[1]), &data) == nil {
info.Runtime = parser.ParseRuntime(data.Full)
}
}
}
})
// Preview Video
c.OnXML(`//*[@id="playerContainer"]/script`, func(e *colly.XMLElement) {
if !strings.Contains(e.Text, "movieId") {
return
}
var movieID, siteID string
if sub := regexp.MustCompile(`movieId\s*=\s*'(\d+?)';`).FindStringSubmatch(e.Text); len(sub) == 2 {
movieID = sub[1]
}
if sub := regexp.MustCompile(`siteID\s*=\s*'(\d+?)';`).FindStringSubmatch(e.Text); len(sub) == 2 {
siteID = sub[1]
}
if movieID == "" || siteID == "" {
return
}
if sub := regexp.MustCompile(`stream\s*=\s*'(.+?)'\+siteID\+'(.+?)'\+movieId\+'(.+?)';`).
FindStringSubmatch(e.Text); len(sub) == 4 {
d := c.Clone()
d.OnResponse(func(r *colly.Response) {
defer func() {
// Sample HLS URL
info.PreviewVideoHLSURL = r.Request.URL.String()
}()
if uri, _, err := m3u8.ParseBestMediaURI(bytes.NewReader(r.Body)); err == nil {
if ss := regexp.MustCompile(`/sample/(\d+)/(\d+)/ts\.(.+?)\.m3u8`).
FindStringSubmatch(uri); len(ss) == 4 {
info.PreviewVideoURL = fmt.Sprintf(sampleURL, ss[1], ss[2], ss[3])
}
}
})
m3u8Link := e.Request.AbsoluteURL(fmt.Sprintf("%s%s%s%s%s", sub[1], siteID, sub[2], movieID, sub[3]))
d.Visit(m3u8Link)
}
})
// Preview Images
c.OnXML(`//div[@class="sample-images yoxview"]/script`, func(e *colly.XMLElement) {
for _, sub := range regexp.MustCompile(`"(/contents/.+/\d+?\.\w+?)"`).FindAllStringSubmatch(e.Text, -1) {
info.PreviewImages = append(info.PreviewImages, e.Request.AbsoluteURL(sub[1]))
}
})
err = c.Visit(info.Homepage)
return
}
func init() {
provider.RegisterMovieFactory(Name, New)
}