Release v0.2.34
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
package api
|
||||
|
||||
import "strings"
|
||||
|
||||
// Where a title's closing credits begin.
|
||||
//
|
||||
// Two sources, in order of trust, because Emby gives one and the media gives the other:
|
||||
//
|
||||
// - `CreditsStart`, a marker Emby's own detector writes. It is in Emby's `MarkerType`
|
||||
// enumeration and is what this feature was originally built on — but **Emby 4.10 does not
|
||||
// write it**. A survey of a 20,000-item library found `Chapter`, `IntroStart` and
|
||||
// `IntroEnd` and nothing else, so the enum value existing is not the detector populating
|
||||
// it. It is still read first, so the day a version does write it this needs no change.
|
||||
// - A chapter *named* like credits. Plenty of media carries "Credits" or "End Credits" as
|
||||
// ordinary chapter metadata, and in that same library 216 items had one, clustered at
|
||||
// 90–98% of runtime and consistent within a show. That is where the feature's coverage
|
||||
// actually comes from today.
|
||||
//
|
||||
// There is deliberately no end marker in either source. Credits run to the end of the file by
|
||||
// definition, so nothing writes one and this must not invent one.
|
||||
|
||||
const markerCreditsStart = "CreditsStart"
|
||||
|
||||
// creditsMinimumPositionFraction is how far into a file a credit roll has to begin.
|
||||
//
|
||||
// **This is the load-bearing guard, and it exists because of one observed case.** Chapter
|
||||
// names are not a vocabulary anybody agreed on, and real media carries "Opening Credits" —
|
||||
// Belfast at 1% of runtime, Game of Thrones at 0%. A name match without a position test
|
||||
// therefore starts the credits pane in the *first minute* of a film and runs its opening at
|
||||
// double speed, which is the worst thing this feature could possibly do.
|
||||
//
|
||||
// Three quarters is deliberately far below the evidence rather than near it: every genuine
|
||||
// credit roll in that survey began at 90% or later, so this leaves fifteen points of headroom
|
||||
// for a long roll while rejecting the whole first half of a file outright.
|
||||
const creditsMinimumPositionFraction = 0.75
|
||||
|
||||
// creditsFromChapters finds where the closing credits begin.
|
||||
//
|
||||
// The rule exists twice — the television's copy is `creditsStartFrom` in `data/Credits.kt` —
|
||||
// and the two are pinned by deliberately parallel tests (`credits_test.go`, `CreditsTest`).
|
||||
// With no gateway there is nobody to ask, and the picture must not start shrinking at a
|
||||
// different moment depending on whether the container is up.
|
||||
//
|
||||
// Most of this is about refusing to answer, and nothing is a perfectly good answer: the player
|
||||
// never shrinks anything and the credits play out full size, which is what every other client
|
||||
// does anyway.
|
||||
//
|
||||
// [runtimeMs] may be zero when Emby does not report one. An explicit marker is still honoured
|
||||
// then — it is Emby asserting a position rather than this inferring one — but a *named*
|
||||
// chapter is refused outright, because the name alone cannot distinguish an opening credit
|
||||
// sequence from a closing one and the position test is the only thing that can.
|
||||
func creditsFromChapters(chapters []embyChapter, runtimeMs int64) (int64, bool) {
|
||||
floor := int64(-1)
|
||||
if runtimeMs > 0 {
|
||||
floor = int64(float64(runtimeMs) * creditsMinimumPositionFraction)
|
||||
}
|
||||
|
||||
// An explicit marker first. The last one wins, where the intro rule takes the first:
|
||||
// two starts mean the markers are untrustworthy, so each rule picks whichever risks
|
||||
// least, and the two features are damaged in opposite directions. An intro skip firing
|
||||
// late throws somebody past the story, so the earlier marker is safer there; the credits
|
||||
// pane firing early runs the last scene past them at double speed, so the later marker is
|
||||
// safer here.
|
||||
marked := int64(-1)
|
||||
for _, chapter := range chapters {
|
||||
if chapter.MarkerType != markerCreditsStart || chapter.StartPositionTicks <= 0 {
|
||||
continue
|
||||
}
|
||||
marked = chapter.StartPositionTicks / ticksPerMillisecond
|
||||
}
|
||||
// A marker below the floor is a mis-detection whoever wrote it, so it falls through to the
|
||||
// names rather than being honoured — but with no runtime to measure against, an explicit
|
||||
// assertion gets the benefit of the doubt.
|
||||
if marked > 0 && (floor < 0 || marked >= floor) {
|
||||
return marked, true
|
||||
}
|
||||
|
||||
if floor < 0 {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// Then the names. The *earliest* qualifying chapter wins here, which is the opposite of
|
||||
// the marker rule above and is not an inconsistency: several credits-named chapters are
|
||||
// ordinary rather than suspicious — "The Pitt" carries both "Credits" and "End Credits" —
|
||||
// and they describe one roll, which begins at the first of them.
|
||||
named := int64(-1)
|
||||
for _, chapter := range chapters {
|
||||
if !isCreditsChapterName(chapter.Name) || chapter.StartPositionTicks <= 0 {
|
||||
continue
|
||||
}
|
||||
at := chapter.StartPositionTicks / ticksPerMillisecond
|
||||
if at < floor {
|
||||
continue
|
||||
}
|
||||
if named < 0 || at < named {
|
||||
named = at
|
||||
}
|
||||
}
|
||||
if named > 0 {
|
||||
return named, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// isCreditsChapterName recognises a chapter that names a credit roll.
|
||||
//
|
||||
// The exclusions are belt-and-braces beside [creditsMinimumPositionFraction], which is what
|
||||
// actually stops an opening sequence being read as a closing one — a position test catches
|
||||
// wordings nobody thought of, where a list of them only catches the ones on the list. They are
|
||||
// here so the trap is stated where the next reader will look for it.
|
||||
func isCreditsChapterName(name string) bool {
|
||||
lowered := strings.ToLower(strings.TrimSpace(name))
|
||||
if lowered == "" {
|
||||
return false
|
||||
}
|
||||
for _, opening := range []string{"opening", "main title", "title sequence", "intro"} {
|
||||
if strings.Contains(lowered, opening) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return strings.Contains(lowered, "credit") ||
|
||||
strings.Contains(lowered, "end titles") ||
|
||||
strings.Contains(lowered, "closing")
|
||||
}
|
||||
Reference in New Issue
Block a user