mirror of
https://github.com/narrrl/proton-stream.git
synced 2026-11-03 16:41:26 +01:00
feat(meta,core,app,android,ios): AniSkip where files have no chapters
Most files that are not fansub releases carry no chapters, so they had no skip button. AniSkip knows where an episode's opening and ending are, by MyAnimeList id and episode number. ani.zip sends each entry's MyAnimeList id and enrichment now keeps it on every episode row; schema v14 adds the column and marks AniList titles un-enriched, deleting nothing, as v13 did. When a file has loaded with no chapter naming an opening or ending, and its length is known, each client asks once, with enrichment on only. The answer lives as long as the file is open. with_skip_ranges cuts it into the chapters as chapters named Opening and Ending, so the skip button, auto-skip, the up-next countdown and the seek bar's marks all work through the code a chaptered file uses. A file's own opening or ending always wins over AniSkip's. PRIVACY.md lists what is sent: the MyAnimeList id, the episode number and the file's length.
This commit is contained in:
1 parent
466630a9ba
commit
cf8ee58f6b
23 files changed
+728
-11
No files matched your search
@@ -120,6 +120,9 @@ final class PlayerHost {
|
||||
@ObservationIgnored private var skipped = Set<Int64>()
|
||||
/// The episode whose next one has been warmed; see `tick`.
|
||||
@ObservationIgnored private var warmedAfter: String?
|
||||
/// The episode AniSkip has been asked about, and what it said.
|
||||
@ObservationIgnored private var skipsAskedFor: String?
|
||||
@ObservationIgnored private var skipRanges: (key: String, ranges: [SkipRangeRecord])?
|
||||
@ObservationIgnored private var pausedByInterruption = false
|
||||
@ObservationIgnored private var artworkFor: String?
|
||||
|
||||
@@ -414,6 +417,26 @@ final class PlayerHost {
|
||||
player.duration - player.position < warmNextSeconds
|
||||
|| chapters.creditsStart.map { player.position >= $0 } == true
|
||||
)
|
||||
// A file whose chapters name no opening or ending asks AniSkip, once,
|
||||
// when its length is known. Only with posters and descriptions on:
|
||||
// anything else, an error included, leaves the file as its chapters
|
||||
// made it.
|
||||
if player.duration > 0, let key, skipsAskedFor != key, let title, let episode {
|
||||
skipsAskedFor = key
|
||||
let named = chapters.entries.contains { $0.kind == .opening || $0.kind == .ending }
|
||||
if !named {
|
||||
let (titleKey, shareId, linkId, duration) = (title.key, episode.shareId, episode.linkId, player.duration)
|
||||
Task { [weak self] in
|
||||
let ranges = (try? await NativeRuntime.engine().skipTimes(
|
||||
titleKey: titleKey, shareId: shareId, linkId: linkId, duration: duration
|
||||
)) ?? []
|
||||
guard let self, !ranges.isEmpty, self.key == key, let player = self.player else { return }
|
||||
self.skipRanges = (key, ranges)
|
||||
self.plannedFor = nil
|
||||
self.replanChapters(player)
|
||||
}
|
||||
}
|
||||
}
|
||||
if nearEnd, let key, warmedAfter != key, let next {
|
||||
warmedAfter = key
|
||||
let (shareId, volumeId, linkId) = (next.shareId, next.volumeId, next.linkId)
|
||||
@@ -431,7 +454,8 @@ final class PlayerHost {
|
||||
let wanted = (player.duration, player.generation)
|
||||
if let plannedFor, plannedFor == wanted, !(chapters.entries.isEmpty && !player.chapters.isEmpty) { return }
|
||||
plannedFor = wanted
|
||||
chapters = chapterPlan(chapters: player.chapters, duration: player.duration > 0 ? player.duration : nil)
|
||||
let skips = skipRanges.flatMap { $0.key == key ? $0.ranges : nil } ?? []
|
||||
chapters = chapterPlan(chapters: player.chapters, duration: player.duration > 0 ? player.duration : nil, skips: skips)
|
||||
}
|
||||
|
||||
/// Play the dialogue rather than the signs and songs mpv picked by
|
||||
|
||||
Reference in new issue
Block a user