Repository: metatube-community/metatube-sdk-go

Issue:
[Bug] The resolution of the preview link is currently the lowest, it used to be the highest
### Verify steps

- [x] Is this something you can **debug and fix**? Send a pull request! Bug fixes and documentation fixes are welcome.
- [x] I have read the [Wiki](https://metatube-community.github.io/wiki/), especially the [FAQ](https://metatube-community.github.io/faq/) page.
- [x] I have searched on the [issue tracker](……/) for a related issue.

### MetaTube Plugin Version

2025.222.2100.0

### MetaTube Server Version

1.2.9

### What OS are you seeing the problem on?

_No response_

### Description

预览片的链接地址分辨率获取是最低了，是否可以获取最高分辨率

### MetaTube Server

_No response_

### Jellyfin/Emby Logs

_No response_

### MetaTube Server Logs

_No response_

### How to Reproduce

_No response_

Discussion:
@xjasonlyu: Fanza的吗？@SaberCenter: Thank you for the quick fix

Solution patch:
diff --git a/common/js/js.go b/common/js/js.go
index c6bff3c..5dbc01a 100644
--- a/common/js/js.go
+++ b/common/js/js.go
@@ -2,11 +2,16 @@ package js
 
 import (
 	"encoding/json"
+	"errors"
 
 	"github.com/robertkrimen/otto"
 )
 
 func UnmarshalObject[T ~string | ~[]byte](jsCode T, objName string, i any) error {
+	if len(jsCode) == 0 {
+		return errors.New("empty JS code snippet")
+	}
+
 	vm := otto.New()
 	v, _ := vm.Run(jsCode)
 
diff --git a/provider/fanza/fanza.go b/provider/fanza/fanza.go
index bc4b5df..a89878a 100644
--- a/provider/fanza/fanza.go
+++ b/provider/fanza/fanza.go
@@ -21,6 +21,7 @@ import (
 
 	"github.com/metatube-community/metatube-sdk-go/collections"
 	"github.com/metatube-community/metatube-sdk-go/common/comparer"
+	"github.com/metatube-community/metatube-sdk-go/common/js"
 	"github.com/metatube-community/metatube-sdk-go/common/number"
 	"github.com/metatube-community/metatube-sdk-go/common/parser"
 	"github.com/metatube-community/metatube-sdk-go/model"
@@ -290,7 +291,7 @@ func (fz *FANZA) GetMovieInfoByURL(rawURL string) (info *model.MovieInfo, err er
 		info.ThumbURL = e.Request.AbsoluteURL(e.Attr("content"))
 	})
 
-	// Preview Video
+	// Preview Video (DVD)
 	c.OnXML(`//*[@id="detail-sample-movie"]/div/a`, func(e *colly.XMLElement) {
 		var videoPath string
 		if dvu := e.Attr("data-video-url"); dvu != "" { // mono
@@ -298,37 +299,51 @@ func (fz *FANZA) GetMovieInfoByURL(rawURL string) (info *model.MovieInfo, err er
 		} else if v := e.Attr("onclick"); v != "" { // digital
 			videoPath = regexp.MustCompile(`/(.+)/`).FindString(v)
 		}
-		d := c.Clone()
-		d.OnXML(`//iframe`, func(e *colly.XMLElement) {
-			d.OnResponse(func(r *colly.Response) {
-				if resp := regexp.MustCompile(`const args = (\{.+});`).FindSubmatch(r.Body); len(resp) == 2 {
-					data := struct {
-						Bitrates []struct {
-							// Bitrate int    `json:"bitrate"`
-							Src string `json:"src"`
-						} `json:"bitrates"`
-					}{}
-					if json.Unmarshal(resp[1], &data) == nil && len(data.Bitrates) > 0 {
-						info.PreviewVideoURL = e.Request.AbsoluteURL(data.Bitrates[0].Src)
-					}
-				}
-			})
-			d.Visit(e.Request.AbsoluteURL(e.Attr("src")))
-		})
-		d.Visit(e.Request.AbsoluteURL(videoPath))
+		info.PreviewVideoURL = fz.parsePreviewVideoURL(c, e.Request.AbsoluteURL(videoPath))
 	})
 
+	// Deprecated (?)
 	// Preview Video (VR)
 	c.OnXML(`//*[@id="detail-sample-vr-movie"]/div/a`, func(e *colly.XMLElement) {
-		d := c.Clone()
-		d.OnResponse(func(r *colly.Response) {
-			sub := regexp.MustCompile(`var sampleUrl = "(.+?)";`).FindSubmatch(r.Body)
-			if len(sub) == 2 {
-				info.PreviewVideoURL = e.Request.AbsoluteURL(string(sub[1]))
+		info.PreviewVideoURL = fz.parseVRPreviewVideoURL(c,
+			e.Request.AbsoluteURL(
+				regexp.MustCompile(`/(.+)/`).FindString(e.Attr("onclick"))))
+	})
+
+	// Preview Video (/digital/video*/)
+	c.OnXML(`//script[@type="text/javascript"]`, func(e *colly.XMLElement) {
+		// // プレイヤー呼び出し
+		// if (autoPlayerMovieFlg) { // 通常プレイヤー
+		// 	sampleplay(`/digital/${autoPlayerFloor}/-/detail/ajax-movie/=/cid=${gaContentId}/`);
+		// } else { // VRプレイヤー
+		// 	vrsampleplay(`/digital/-/vr-sample-player/=/cid=${gaContentId}/`);
+		// }
+		if jsCodes := regexp.MustCompile(`const\s+.+?\s*=.*?;`).FindAllString(e.Text, -1); len(jsCodes) > 0 {
+			var (
+				autoPlayerMovieFlg bool
+				autoPlayerFloor    string
+			)
+			extractCode := func(v string) string {
+				for _, jsCode := range jsCodes {
+					if strings.Contains(jsCode, v) {
+						return strings.ReplaceAll(jsCode, "const ", "var ")
+					}
+				}
+				return ""
 			}
-		})
-		d.Visit(e.Request.AbsoluteURL(regexp.MustCompile(`/(.+)/`).
-			FindString(e.Attr("onclick"))))
+			_ = js.UnmarshalObject(extractCode("autoPlayerFloor"), "autoPlayerFloor", &autoPlayerFloor)
+			_ = js.UnmarshalObject(extractCode("autoPlayerMovieFlg"), "autoPlayerMovieFlg", &autoPlayerMovieFlg)
+			if autoPlayerFloor == "" {
+				return // skip
+			}
+			if autoPlayerMovieFlg {
+				sampleURL := e.Request.AbsoluteURL(fmt.Sprintf(`/digital/%s/-/detail/ajax-movie/=/cid=%s/`, autoPlayerFloor, info.ID))
+				info.PreviewVideoURL = fz.parsePreviewVideoURL(c, sampleURL)
+			} else {
+				vrSampleURL := e.Request.AbsoluteURL(fmt.Sprintf(`/digital/-/vr-sample-player/=/cid=%s/`, info.ID))
+				info.PreviewVideoURL = fz.parseVRPreviewVideoURL(c, vrSampleURL)
+			}
+		}
 	})
 
 	// In case of any duplication
@@ -651,6 +666,40 @@ func (fz *FANZA) parseScoreFromURL(s string) float64 {
 	return score
 }
 
+func (fz *FANZA) parsePreviewVideoURL(c *colly.Collector, iFrameURL string) (previewVideoURL string) {
+	d := c.Clone()
+	d.OnXML(`//iframe`, func(e *colly.XMLElement) {
+		d.OnResponse(func(r *colly.Response) {
+			if resp := regexp.MustCompile(`const args = (\{.+});`).FindSubmatch(r.Body); len(resp) == 2 {
+				data := struct {
+					Bitrates []struct {
+						// Bitrate int    `json:"bitrate"`
+						Src string `json:"src"`
+					} `json:"bitrates"`
+				}{}
+				if json.Unmarshal(resp[1], &data) == nil && len(data.Bitrates) > 0 {
+					previewVideoURL = e.Request.AbsoluteURL(data.Bitrates[0].Src)
+				}
+			}
+		})
+		d.Visit(e.Request.AbsoluteURL(e.Attr("src")))
+	})
+	d.Visit(iFrameURL)
+	return
+}
+
+func (fz *FANZA) parseVRPreviewVideoURL(c *colly.Collector, vrVideoURL string) (previewVideoURL string) {
+	d := c.Clone()
+	d.OnResponse(func(r *colly.Response) {
+		sub := regexp.MustCompile(`var sampleUrl = "(.+?)";`).FindSubmatch(r.Body)
+		if len(sub) == 2 {
+			previewVideoURL = r.Request.AbsoluteURL(string(sub[1]))
+		}
+	})
+	d.Visit(vrVideoURL)
+	return
+}
+
 // ParseNumber parses FANZA-formatted id to general ID.
 func ParseNumber(s string) string {
 	if ss := regexp.MustCompile(`([A-Z]+)(\d+)`).FindStringSubmatch(strings.ToUpper(s)); len(ss) >= 3 {



Test patch:
diff --git a/provider/fanza/fanza_test.go b/provider/fanza/fanza_test.go
index b8eb518..d3e524d 100644
--- a/provider/fanza/fanza_test.go
+++ b/provider/fanza/fanza_test.go
@@ -33,6 +33,9 @@ func TestFANZA_GetMovieInfoByID(t *testing.T) {
 func TestFANZA_GetMovieInfoByURL(t *testing.T) {
 	testkit.Test(t, New, []string{
 		"https://www.dmm.co.jp/digital/videoa/-/detail/=/cid=1start00190/",
+		"https://www.dmm.co.jp/digital/videoc/-/detail/=/cid=neki004/",
+		"https://www.dmm.co.jp/digital/videoa/-/detail/=/cid=mdvr00344/",
+		"https://www.dmm.co.jp/mono/dvd/-/detail/=/cid=k9sone740/",
 		"https://www.dmm.co.jp/digital/videoa/-/detail/=/cid=ebwh00164/",
 		"https://www.dmm.co.jp/digital/videoa/-/detail/=/cid=fpre00106/",
 		"https://www.dmm.co.jp/mono/dvd/-/detail/=/cid=41hodv21810/",