Repository: metatube-community/metatube-sdk-go

Issue:
[Feature] Request to add data source:FC2PPVDB
### Description

Request to add data source FC2PPVDB
The link is: https://fc2ppvdb.com

This is currently the only relatively complete FC2 video database available. It includes discontinued videos. Apart from early content, newly released videos are mostly covered. Moreover, the information is relatively comprehensive.

### Is this feature related to a specific bug?

_No response_

### Do you have a specific solution in mind?

_No response_

Hints from issue comments:

Solution patch:
diff --git a/engine/register.go b/engine/register.go
index 52e9ae4..b7d360c 100644
--- a/engine/register.go
+++ b/engine/register.go
@@ -17,6 +17,7 @@ import (
 	_ "github.com/metatube-community/metatube-sdk-go/provider/fanza"
 	_ "github.com/metatube-community/metatube-sdk-go/provider/fc2"
 	_ "github.com/metatube-community/metatube-sdk-go/provider/fc2hub"
+	_ "github.com/metatube-community/metatube-sdk-go/provider/fc2ppvdb"
 	_ "github.com/metatube-community/metatube-sdk-go/provider/gcolle"
 	_ "github.com/metatube-community/metatube-sdk-go/provider/getchu"
 	_ "github.com/metatube-community/metatube-sdk-go/provider/gfriends"
diff --git a/provider/fc2/fc2.go b/provider/fc2/fc2.go
index 8869c84..28046a8 100644
--- a/provider/fc2/fc2.go
+++ b/provider/fc2/fc2.go
@@ -2,6 +2,7 @@ package fc2
 
 import (
 	"fmt"
+	"net/http"
 	"net/url"
 	"path"
 	"regexp"
@@ -187,6 +188,10 @@ func (fc2 *FC2) GetMovieInfoByURL(rawURL string) (info *model.MovieInfo, err err
 	return
 }
 
+func (fc2 *FC2) Fetch(url string) (*http.Response, error) {
+	return fc2util.FetchImage(url)
+}
+
 func init() {
 	provider.Register(Name, New)
 }
diff --git a/provider/fc2/fc2util/fc2-no-image.png b/provider/fc2/fc2util/fc2-no-image.png
new file mode 100644
index 0000000..97b507c
Binary files /dev/null and b/provider/fc2/fc2util/fc2-no-image.png differ
diff --git a/provider/fc2/fc2util/fc2util.go b/provider/fc2/fc2util/fc2util.go
index 566c66a..2df0afd 100644
--- a/provider/fc2/fc2util/fc2util.go
+++ b/provider/fc2/fc2util/fc2util.go
@@ -1,14 +1,45 @@
 package fc2util
 
 import (
+	"bytes"
+	_ "embed"
+	"io"
+	"net/http"
 	"regexp"
+	"strings"
 )
 
-var _fc2pattern = regexp.MustCompile(`^(?i)(?:FC2(?:[-_\s]?PPV)?[-_\s]?)?(\d+)$`)
+var fc2pattern = regexp.MustCompile(`^(?i)(?:FC2(?:[-_\s]?PPV)?[-_\s]?)?(\d+)$`)
 
-func ParseNumber(s string) (n string) {
-	if ss := _fc2pattern.FindStringSubmatch(s); len(ss) == 2 {
-		n = ss[1]
+func ParseNumber(id string) string {
+	ss := fc2pattern.FindStringSubmatch(id)
+	if len(ss) != 2 {
+		return ""
 	}
-	return
+	return ss[1]
+}
+
+//go:embed fc2-no-image.png
+var fc2NoImage []byte
+
+func FetchImage(url string) (*http.Response, error) {
+	resp, err := http.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	if !strings.HasSuffix(url, ".jpg") {
+		// skip if not requesting an image.
+		return resp, nil
+	}
+	if resp.StatusCode == http.StatusOK ||
+		strings.HasPrefix(resp.Header.Get("Content-Type"), "image/") {
+		return resp, nil
+	}
+	defer resp.Body.Close()
+
+	// return no-image content.
+	return &http.Response{
+		StatusCode: http.StatusOK,
+		Body:       io.NopCloser(bytes.NewReader(fc2NoImage)),
+	}, nil
 }
diff --git a/provider/fc2hub/fc2hub.go b/provider/fc2hub/fc2hub.go
index e973f67..818358f 100644
--- a/provider/fc2hub/fc2hub.go
+++ b/provider/fc2hub/fc2hub.go
@@ -238,6 +238,10 @@ func (fc2hub *FC2HUB) SearchMovie(keyword string) (results []*model.MovieSearchR
 	return
 }
 
+func (fc2hub *FC2HUB) Fetch(url string) (*http.Response, error) {
+	return fc2util.FetchImage(url)
+}
+
 func removeEmpty(in []string) (out []string) {
 	if len(in) == 0 {
 		return in
diff --git a/provider/fc2ppvdb/fc2ppvdb.go b/provider/fc2ppvdb/fc2ppvdb.go
new file mode 100644
index 0000000..63ea463
--- /dev/null
+++ b/provider/fc2ppvdb/fc2ppvdb.go
@@ -0,0 +1,123 @@
+package fc2ppvdb
+
+import (
+	"fmt"
+	"net/http"
+	"net/url"
+	"path"
+	"strings"
+
+	"github.com/antchfx/htmlquery"
+	"github.com/gocolly/colly/v2"
+	"golang.org/x/net/html"
+	"golang.org/x/text/language"
+
+	"github.com/metatube-community/metatube-sdk-go/common/parser"
+	"github.com/metatube-community/metatube-sdk-go/model"
+	"github.com/metatube-community/metatube-sdk-go/provider"
+	"github.com/metatube-community/metatube-sdk-go/provider/fc2/fc2util"
+	"github.com/metatube-community/metatube-sdk-go/provider/internal/scraper"
+)
+
+var _ provider.MovieProvider = (*FC2PPVDB)(nil)
+
+const (
+	Name     = "FC2PPVDB"
+	Priority = 1000 - 2
+)
+
+const (
+	baseURL  = "https://fc2ppvdb.com/"
+	movieURL = "https://fc2ppvdb.com/articles/%s"
+)
+
+type FC2PPVDB struct {
+	*scraper.Scraper
+}
+
+func New() *FC2PPVDB {
+	return &FC2PPVDB{scraper.NewDefaultScraper(Name, baseURL, Priority, language.Japanese)}
+}
+
+func (fc2ppvdb *FC2PPVDB) NormalizeMovieID(id string) string {
+	return fc2util.ParseNumber(id)
+}
+
+func (fc2ppvdb *FC2PPVDB) GetMovieInfoByID(id string) (info *model.MovieInfo, err error) {
+	return fc2ppvdb.GetMovieInfoByURL(fmt.Sprintf(movieURL, id))
+}
+
+func (fc2ppvdb *FC2PPVDB) ParseMovieIDFromURL(rawURL string) (string, error) {
+	homepage, err := url.Parse(rawURL)
+	if err != nil {
+		return "", err
+	}
+	return path.Base(homepage.Path), nil
+}
+
+func (fc2ppvdb *FC2PPVDB) GetMovieInfoByURL(rawURL string) (info *model.MovieInfo, err error) {
+	id, err := fc2ppvdb.ParseMovieIDFromURL(rawURL)
+	if err != nil {
+		return
+	}
+
+	info = &model.MovieInfo{
+		ID:            id,
+		Number:        fmt.Sprintf("FC2-%s", id),
+		Provider:      fc2ppvdb.Name(),
+		Homepage:      rawURL,
+		Actors:        []string{},
+		PreviewImages: []string{},
+		Genres:        []string{},
+	}
+
+	c := fc2ppvdb.ClonedCollector()
+
+	// Cover/Thumb Image
+	c.OnXML(`//main//div[contains(@class,'container')]/div[1]/div[1]/a/img`, func(e *colly.XMLElement) {
+		info.CoverURL = e.Request.AbsoluteURL(e.Attr("src"))
+	})
+
+	// Title
+	c.OnXML(`//main//div[contains(@class,'container')]/div[1]/div[2]/h2/a`, func(e *colly.XMLElement) {
+		info.Title = e.Text
+	})
+
+	// Fields
+	c.OnXML(`//main//div[contains(@class,'container')]/div[1]/div[2]/div`, func(e *colly.XMLElement) {
+		if child := e.DOM.(*html.Node).FirstChild; child != nil {
+			switch child.Data {
+			case "ID：":
+				info.ID = strings.TrimSpace(e.ChildText(`.//span`))
+			case "販売者：":
+				info.Maker = strings.TrimSpace(e.ChildText(`.//span`))
+			case "女優：":
+				parser.ParseTexts(
+					htmlquery.FindOne(e.DOM.(*html.Node), `.//span`),
+					(*[]string)(&info.Actors),
+				)
+			case "モザイク：": // mosaic
+			case "販売日：":
+				info.ReleaseDate = parser.ParseDate(e.ChildText(`.//span`))
+			case "収録時間：":
+				info.Runtime = parser.ParseRuntime(e.ChildText(`.//span`))
+			case "タグ：": // tags & genres
+				parser.ParseTexts(
+					htmlquery.FindOne(e.DOM.(*html.Node), `.//span`),
+					(*[]string)(&info.Genres),
+				)
+			}
+		}
+	})
+
+	err = c.Visit(info.Homepage)
+	return
+}
+
+func (fc2ppvdb *FC2PPVDB) Fetch(url string) (*http.Response, error) {
+	return fc2util.FetchImage(url)
+}
+
+func init() {
+	provider.Register(Name, New)
+}



Test patch:
diff --git a/provider/fc2/fc2util/fc2util_test.go b/provider/fc2/fc2util/fc2util_test.go
index 5e09938..ab950dc 100644
--- a/provider/fc2/fc2util/fc2util_test.go
+++ b/provider/fc2/fc2util/fc2util_test.go
@@ -1,6 +1,7 @@
 package fc2util
 
 import (
+	"io"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -31,3 +32,18 @@ func TestParseNumber(t *testing.T) {
 		assert.Equal(t, unit.want, ParseNumber(unit.orig), unit.orig)
 	}
 }
+
+func TestFetchImage(t *testing.T) {
+	for _, unit := range []struct {
+		url  string
+		want []byte
+	}{
+		{"https://storage89000.contents.fc2.com/file/392/39188234/1702896500.66.jpg", fc2NoImage},
+	} {
+		resp, err := FetchImage(unit.url)
+		if assert.NoError(t, err) {
+			data, _ := io.ReadAll(resp.Body)
+			assert.Equal(t, unit.want, data)
+		}
+	}
+}
diff --git a/provider/fc2ppvdb/fc2ppvdb_test.go b/provider/fc2ppvdb/fc2ppvdb_test.go
new file mode 100644
index 0000000..3b21161
--- /dev/null
+++ b/provider/fc2ppvdb/fc2ppvdb_test.go
@@ -0,0 +1,16 @@
+package fc2ppvdb
+
+import (
+	"testing"
+
+	"github.com/metatube-community/metatube-sdk-go/provider/internal/testkit"
+)
+
+func TestFC2PPVDB_GetMovieInfoByID(t *testing.T) {
+	testkit.Test(t, New, []string{
+		"2812904",
+		"4669533",
+		"4745474",
+		"4137487",
+	})
+}