Release v0.2.34

This commit is contained in:
ponzischeme89
2026-08-09 08:25:50 +12:00
parent b1128bcce2
commit fdd9e6cab2
116 changed files with 11418 additions and 879 deletions
+124
View File
@@ -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
// 9098% 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")
}