mirror of
https://github.com/truewhile/MeBox.git
synced 2026-11-13 11:54:22 +08:00
Compare commits
6 Commits
mebox-v0.2.1
...
beta
| Author | SHA1 | Date | |
|---|---|---|---|
| a7b24e1bf1 | |||
| 0b68397265 | |||
| 90bfac3b09 | |||
| f1974a179e | |||
| cd6b37c6a0 | |||
| ef8b017c63 |
@@ -22,8 +22,9 @@ import (
|
||||
|
||||
const (
|
||||
// IntroDBBaseURL is the public read endpoint. Overridable on the service
|
||||
// for tests and for pointing at a mirror.
|
||||
IntroDBBaseURL = "https://api.theintrodb.org/v3"
|
||||
// for tests and for pointing at a mirror. v1 已删除,v2/v3 冻结在只读的
|
||||
// GET /media 上并被官方标记为弃用,所以这里直接跟 v4。
|
||||
IntroDBBaseURL = "https://api.theintrodb.org/v4"
|
||||
// IntroDBSource tags rows that came from this provider.
|
||||
IntroDBSource = "theintrodb"
|
||||
|
||||
@@ -92,8 +93,10 @@ type introDBRange struct {
|
||||
EndMs *int64 `json:"end_ms"`
|
||||
}
|
||||
|
||||
// introDBResponse 对应 v4 的 MediaResponse。v4 以 TVDB 为身份,响应只带
|
||||
// tvdb_id —— tmdb_id 字段已被官方移除,需要 tmdb_id 只能从请求侧自己记。
|
||||
type introDBResponse struct {
|
||||
TMDbID int `json:"tmdb_id"`
|
||||
TVDBID int `json:"tvdb_id"`
|
||||
Type string `json:"type"`
|
||||
Intro []introDBRange `json:"intro"`
|
||||
Recap []introDBRange `json:"recap"`
|
||||
|
||||
@@ -11,11 +11,12 @@ import (
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// 这两段响应是从 api.theintrodb.org/v3/media 实测抓下来的原文,
|
||||
// 用来锁住 null 语义:start_ms: null = 从片头开始,end_ms: null = 一直到片尾。
|
||||
// 这两段响应是从 api.theintrodb.org/v4/media 实测抓下来的原文(v4 以 TVDB 为身份,
|
||||
// 响应里只有 tvdb_id),用来锁住 null 语义:start_ms: null = 从片头开始,
|
||||
// end_ms: null = 一直到片尾。
|
||||
const (
|
||||
introDBTVPayload = `{"tmdb_id":1396,"type":"tv","season":1,"episode":1,"intro":[{"start_ms":228664,"end_ms":246143}],"credits":[{"start_ms":3431000,"end_ms":null}]}`
|
||||
introDBMoviePayload = `{"tmdb_id":27205,"type":"movie","intro":[{"start_ms":null,"end_ms":38000}]}`
|
||||
introDBTVPayload = `{"tvdb_id":81189,"type":"tv","season":1,"episode":1,"intro":[{"start_ms":228664,"end_ms":246143}],"credits":[{"start_ms":3431000,"end_ms":null}]}`
|
||||
introDBMoviePayload = `{"tvdb_id":113,"type":"movie","intro":[{"start_ms":null,"end_ms":38000}]}`
|
||||
)
|
||||
|
||||
func TestParseIntroDBResponseResolvesNullBounds(t *testing.T) {
|
||||
@@ -48,7 +49,7 @@ func TestParseIntroDBResponseResolvesNullBounds(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseIntroDBResponseDropsEmptyRanges(t *testing.T) {
|
||||
body := `{"tmdb_id":1,"type":"movie",
|
||||
body := `{"tvdb_id":1,"type":"movie",
|
||||
"intro":[{"start_ms":5000,"end_ms":5000},{"start_ms":9000,"end_ms":8000},{"start_ms":1000,"end_ms":2000}],
|
||||
"recap":[],"credits":[],"preview":[]}`
|
||||
spans, err := parseIntroDBResponse([]byte(body))
|
||||
@@ -62,7 +63,7 @@ func TestParseIntroDBResponseDropsEmptyRanges(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseIntroDBResponseOrdersByType(t *testing.T) {
|
||||
body := `{"tmdb_id":1,"type":"tv","credits":[{"start_ms":900,"end_ms":1000}],
|
||||
body := `{"tvdb_id":1,"type":"tv","credits":[{"start_ms":900,"end_ms":1000}],
|
||||
"intro":[{"start_ms":100,"end_ms":200}],"recap":[{"start_ms":50,"end_ms":60}]}`
|
||||
spans, err := parseIntroDBResponse([]byte(body))
|
||||
if err != nil {
|
||||
@@ -82,12 +83,12 @@ func TestParseIntroDBResponseOrdersByType(t *testing.T) {
|
||||
func TestIntroDBMediaURLOnlyAddsSeasonEpisodeForTV(t *testing.T) {
|
||||
svc := NewIntroDBService(zap.NewNop())
|
||||
if got, want := svc.mediaURL(1396, 1, 1),
|
||||
"https://api.theintrodb.org/v3/media?tmdb_id=1396&season=1&episode=1"; got != want {
|
||||
"https://api.theintrodb.org/v4/media?tmdb_id=1396&season=1&episode=1"; got != want {
|
||||
t.Fatalf("tv url = %q, want %q", got, want)
|
||||
}
|
||||
// 电影(season/episode 为 0)不能带季集参数,否则会被当成剧集查不到。
|
||||
if got, want := svc.mediaURL(27205, 0, 0),
|
||||
"https://api.theintrodb.org/v3/media?tmdb_id=27205"; got != want {
|
||||
"https://api.theintrodb.org/v4/media?tmdb_id=27205"; got != want {
|
||||
t.Fatalf("movie url = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,6 +184,85 @@ func TestReadAdultMetadataDerivesDMMPosterFromRemoteFanart(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadAdultArtworkIgnoresLoosePosterInLibraryRoot(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
// 云下载这类库把不同影片平铺在同一个根目录里,根目录中残留的松散海报
|
||||
// 属于别的影片,不能被抓来当作当前影片的封面。
|
||||
if err := os.WriteFile(filepath.Join(root, "AAA-001-poster.jpg"), []byte("jpg"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(root, "AAA-001-fanart.jpg"), []byte("jpg"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mediaPath := filepath.Join(root, "BBB-002.mp4")
|
||||
if err := os.WriteFile(mediaPath, []byte("x"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got, err := ReadLocalMetadata(mediaPath, root, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != nil && (got.PosterURL != "" || got.BackdropURL != "") {
|
||||
t.Fatalf("library-root artwork must not be inherited: poster=%q backdrop=%q", got.PosterURL, got.BackdropURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadAdultArtworkIgnoresLoosePosterInSharedFolder(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
dir := filepath.Join(root, "shared")
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, name := range []string{"AAA-001.mp4", "BBB-002.mp4"} {
|
||||
if err := os.WriteFile(filepath.Join(dir, name), []byte("x"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "AAA-001-poster.jpg"), []byte("jpg"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "AAA-001-fanart.jpg"), []byte("jpg"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got, err := ReadLocalMetadata(filepath.Join(dir, "BBB-002.mp4"), root, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != nil && (got.PosterURL != "" || got.BackdropURL != "") {
|
||||
t.Fatalf("sibling artwork must not be inherited in a shared folder: poster=%q backdrop=%q", got.PosterURL, got.BackdropURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadAdultArtworkKeepsLooseFallbackInItemFolder(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
dir := filepath.Join(root, "BBB-002")
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mediaPath := filepath.Join(dir, "video.mp4")
|
||||
if err := os.WriteFile(mediaPath, []byte("x"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
poster := filepath.Join(dir, "japanese-title-cover.jpg")
|
||||
fanart := filepath.Join(dir, "japanese-title-fanart.jpg")
|
||||
if err := os.WriteFile(poster, []byte("jpg"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(fanart, []byte("jpg"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got, err := ReadLocalMetadata(mediaPath, root, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got == nil || got.PosterURL != poster || got.BackdropURL != fanart {
|
||||
t.Fatalf("single-item folder should keep loose artwork fallback: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadAdultArtworkByCodeWithoutNFO(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
mediaPath := filepath.Join(root, "SSIS-001-CD1.mp4")
|
||||
|
||||
@@ -11,23 +11,23 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func metadataFromArtwork(mediaPath, showBaseDir string) *LocalMetadata {
|
||||
func metadataFromArtwork(mediaPath, showBaseDir, libraryRoot string) *LocalMetadata {
|
||||
meta := &LocalMetadata{}
|
||||
mergeArtworkMetadata(meta, mediaPath, showBaseDir)
|
||||
mergeArtworkMetadata(meta, mediaPath, showBaseDir, libraryRoot)
|
||||
if meta.PosterURL == "" && meta.BackdropURL == "" {
|
||||
return nil
|
||||
}
|
||||
return meta
|
||||
}
|
||||
|
||||
func mergeArtworkMetadata(meta *LocalMetadata, mediaPath, showBaseDir string) {
|
||||
func mergeArtworkMetadata(meta *LocalMetadata, mediaPath, showBaseDir, libraryRoot string) {
|
||||
if meta == nil {
|
||||
return
|
||||
}
|
||||
mediaDir := filepath.Dir(mediaPath)
|
||||
if localPoster := firstLocalPoster(mediaPath, showBaseDir); localPoster != "" {
|
||||
meta.PosterURL = localPoster
|
||||
} else if meta.PosterURL == "" {
|
||||
} else if meta.PosterURL == "" && looseArtworkFallbackAllowed(mediaDir, libraryRoot) {
|
||||
meta.PosterURL = firstAdultLooseImage(mediaDir, "poster")
|
||||
}
|
||||
dirs := []string{mediaDir, showBaseDir}
|
||||
@@ -39,7 +39,7 @@ func mergeArtworkMetadata(meta *LocalMetadata, mediaPath, showBaseDir string) {
|
||||
meta.BackdropURL = img
|
||||
break
|
||||
}
|
||||
if meta.BackdropURL == "" {
|
||||
if meta.BackdropURL == "" && looseArtworkFallbackAllowed(dir, libraryRoot) {
|
||||
meta.BackdropURL = firstAdultLooseImage(dir, "backdrop")
|
||||
}
|
||||
}
|
||||
@@ -245,6 +245,48 @@ func firstExistingPosterImage(dir string, names ...string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// looseArtworkFallbackAllowed reports whether an unmatched image sitting in dir
|
||||
// can be attributed to mediaPath. Such an image carries no filename relation to
|
||||
// a specific video, so it is only trustworthy when the folder holds a single
|
||||
// media file and that folder is not the library root. Download tools routinely
|
||||
// drop unrelated titles flat into one directory (a 115 cloud-download library,
|
||||
// for example); claiming the first "*-poster.jpg" there made every item show the
|
||||
// same artwork. Mirrors the root-level guard findMovieNFO already applies to
|
||||
// loose NFO files.
|
||||
func looseArtworkFallbackAllowed(dir, libraryRoot string) bool {
|
||||
if dir == "" {
|
||||
return false
|
||||
}
|
||||
if libraryRoot != "" && samePath(dir, libraryRoot) {
|
||||
return false
|
||||
}
|
||||
return countMediaFilesInDir(dir) <= 1
|
||||
}
|
||||
|
||||
// countMediaFilesInDir counts the video files directly inside dir, stopping once
|
||||
// more than one is found. It walks the real directory rather than reusing the
|
||||
// loose-image glob because a shared folder may contain no images at all.
|
||||
func countMediaFilesInDir(dir string) int {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
count := 0
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
if _, ok := videoExtensions[strings.ToLower(filepath.Ext(entry.Name()))]; !ok {
|
||||
continue
|
||||
}
|
||||
count++
|
||||
if count > 1 {
|
||||
return count
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func firstAdultLooseImage(dir, kind string) string {
|
||||
if dir == "" {
|
||||
return ""
|
||||
|
||||
@@ -17,12 +17,12 @@ func ReadLocalMetadata(mediaPath, libraryRoot string, seriesLike bool) (*LocalMe
|
||||
doc, path, err := findMovieNFO(mediaPath, libraryRoot)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return metadataFromArtwork(mediaPath, ""), nil
|
||||
return metadataFromArtwork(mediaPath, "", libraryRoot), nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
meta := metadataFromDoc(doc, filepath.Dir(path), false)
|
||||
mergeArtworkMetadata(meta, mediaPath, filepath.Dir(path))
|
||||
mergeArtworkMetadata(meta, mediaPath, filepath.Dir(path), libraryRoot)
|
||||
// A sidecar written while resolution tokens were misread as SxxExx must not
|
||||
// reintroduce the bogus season/episode on rescan.
|
||||
dropResolutionArtifactEpisodeIdentity(meta, mediaPath)
|
||||
@@ -115,9 +115,9 @@ func readSeriesMetadata(mediaPath, libraryRoot string) (*LocalMetadata, error) {
|
||||
}
|
||||
}
|
||||
if meta == nil {
|
||||
meta = metadataFromArtwork(mediaPath, showBaseDir)
|
||||
meta = metadataFromArtwork(mediaPath, showBaseDir, libraryRoot)
|
||||
} else {
|
||||
mergeArtworkMetadata(meta, mediaPath, showBaseDir)
|
||||
mergeArtworkMetadata(meta, mediaPath, showBaseDir, libraryRoot)
|
||||
}
|
||||
// A sidecar written while resolution tokens were misread as SxxExx must not
|
||||
// reintroduce the bogus season/episode on rescan.
|
||||
|
||||
Reference in New Issue
Block a user