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

259 lines
6.1 KiB
Go

package duga
import (
"encoding/json"
"fmt"
"net/url"
"path"
"strings"
"sync"
"time"
"github.com/gocolly/colly/v2"
"github.com/javtube/metatube-sdk-go/common/number"
"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 = (*DUGA)(nil)
_ provider.MovieSearcher = (*DUGA)(nil)
)
const (
Name = "DUGA"
Priority = 1000 - 2
)
const (
baseURL = "https://duga.jp/"
movieURL = "https://duga.jp/ppv/%s/"
searchURL = "https://duga.jp/search/=/q=%s/"
)
type DUGA struct {
*scraper.Scraper
}
func New() *DUGA {
return &DUGA{
Scraper: scraper.NewDefaultScraper(Name, baseURL, Priority),
}
}
func (duga *DUGA) NormalizeID(id string) string {
return strings.ToLower(id) // DUGA always use lowercase id.
}
func (duga *DUGA) GetMovieInfoByID(id string) (info *model.MovieInfo, err error) {
return duga.GetMovieInfoByURL(fmt.Sprintf(movieURL, id))
}
func (duga *DUGA) ParseIDFromURL(rawURL string) (string, error) {
homepage, err := url.Parse(rawURL)
if err != nil {
return "", err
}
return duga.NormalizeID(path.Base(homepage.Path)), nil
}
func (duga *DUGA) GetMovieInfoByURL(rawURL string) (info *model.MovieInfo, err error) {
id, err := duga.ParseIDFromURL(rawURL)
if err != nil {
return
}
info = &model.MovieInfo{
ID: id,
Provider: duga.Name(),
Homepage: rawURL,
Actors: []string{},
PreviewImages: []string{},
Genres: []string{},
}
c := duga.ClonedCollector()
// Title
c.OnXML(`//*[@id="contentsname"]`, func(e *colly.XMLElement) {
info.Title = e.Text
})
// Summary+JSON
c.OnXML(`//script[@type="application/ld+json"]`, func(e *colly.XMLElement) {
data := struct {
// Name string `json:"name"`
Description string `json:"description"`
}{}
if json.NewDecoder(strings.NewReader(e.Text)).Decode(&data) == nil {
info.Summary = data.Description
}
})
// Thumb
c.OnXML(`//div[@class="imagebox"]//img[@id="productjpg"]`, func(e *colly.XMLElement) {
info.ThumbURL = e.Request.AbsoluteURL(e.Attr("src"))
})
// Cover
c.OnXML(`//div[@class="imagebox"]/a`, func(e *colly.XMLElement) {
info.CoverURL = e.Request.AbsoluteURL(e.Attr("href"))
})
// Fields
c.OnXML(`//div[@class="summaryinner"]//table//tr`, func(e *colly.XMLElement) {
switch e.ChildText(`.//th`) {
case "配信開始日", "発売日":
if time.Time(info.ReleaseDate).IsZero() {
info.ReleaseDate = parser.ParseDate(e.ChildText(`.//td`))
}
case "メーカー":
info.Maker = strings.TrimSpace(e.ChildText(`.//td`))
case "レーベル":
info.Label = strings.TrimSpace(e.ChildText(`.//td`))
case "作品ID":
info.ID = strings.TrimSpace(e.ChildText(`.//td`))
case "メーカー品番":
info.Number = strings.TrimSpace(e.ChildText(`.//td`))
case "シリーズ":
info.Series = strings.TrimSpace(e.ChildText(`.//td`))
case "出演者", "監督", "カテゴリ":
// parse later.
}
})
// Director
c.OnXML(`//ul[@class="director"]//li//a`, func(e *colly.XMLElement) {
if info.Director != "" {
return // ignore others.
}
info.Director = e.Text
})
// Actors
c.OnXML(`//ul[@class="performer"]//li//a`, func(e *colly.XMLElement) {
info.Actors = append(info.Actors, e.Text)
})
// Genres
c.OnXML(`//ul[@class="categorylist"]//li//a`, func(e *colly.XMLElement) {
info.Genres = append(info.Genres, e.Text)
})
// Title (fallback)
c.OnXML(`//meta[@property="og:title"]`, func(e *colly.XMLElement) {
if info.Title != "" {
return
}
info.Title = e.Attr("content")
})
// Summary (fallback)
c.OnXML(`//meta[@property="og:description"]`, func(e *colly.XMLElement) {
if info.Summary != "" {
return
}
info.Summary = e.Attr("content")
})
// Thumb (fallback)
c.OnXML(`//meta[@property="og:image"]`, func(e *colly.XMLElement) {
if info.ThumbURL != "" {
return
}
info.ThumbURL = e.Request.AbsoluteURL(e.Attr("content"))
})
// Score
c.OnXML(`//div[@class="summaryinner"]//div[@class="ratingstar-total"]`, func(e *colly.XMLElement) {
info.Score = parser.ParseScore(e.ChildAttr(`.//img`, "alt"))
})
// Runtime
c.OnXML(`//div[@class="downloadbox"]//table//tr`, func(e *colly.XMLElement) {
if info.Runtime > 0 {
return
}
if e.ChildText(`.//th`) == "再生時間" {
info.Runtime = parser.ParseRuntime(e.ChildText(`.//td`))
}
})
// Preview Video
c.OnXML(`//video[@class="play-video"]`, func(e *colly.XMLElement) {
info.PreviewVideoURL = e.Request.AbsoluteURL(e.Attr("src"))
})
// Preview Images
c.OnXML(`//*[@id="digestthumbbox"]/li`, func(e *colly.XMLElement) {
info.PreviewImages = append(info.PreviewImages,
e.Request.AbsoluteURL(e.ChildAttr(`.//a`, "href")))
})
// Multiple (fallback)
c.OnScraped(func(_ *colly.Response) {
if info.CoverURL == "" {
// use thumb as cover.
info.CoverURL = info.ThumbURL
}
if info.ID == "" {
// fallback to id again.
info.ID = id
}
if info.Number == "" {
// use ID as number.
info.Number = strings.ToUpper(info.ID)
}
})
err = c.Visit(info.Homepage)
return
}
func (duga *DUGA) NormalizeKeyword(keyword string) string {
if number.IsSpecial(keyword) {
return ""
}
return strings.ToUpper(keyword)
}
func (duga *DUGA) SearchMovie(keyword string) (results []*model.MovieSearchResult, err error) {
c := duga.ClonedCollector()
var ids []string
c.OnXML(`//*[@id="searchresultarea"]//div[@class="contentslist"]`, func(e *colly.XMLElement) {
id, _ := duga.ParseIDFromURL(e.Request.AbsoluteURL(e.ChildAttr(`.//a`, "href")))
ids = append(ids, id)
})
c.OnScraped(func(r *colly.Response) {
const limit = 3
var (
mu sync.Mutex
wg sync.WaitGroup
)
for i := 0; i < len(ids) && i < limit; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
if info, _ := duga.GetMovieInfoByID(ids[i]); info != nil && info.Valid() {
mu.Lock()
results = append(results, info.ToSearchResult())
mu.Unlock()
}
}(i)
}
wg.Wait()
})
err = c.Visit(fmt.Sprintf(searchURL, keyword))
return
}
func init() {
provider.RegisterMovieFactory(Name, New)
}