mirror of
https://github.com/yattee/yattee.git
synced 2026-11-02 18:07:19 +00:00
Start a session where the watch actually begins
This commit is contained in:
@@ -264,13 +264,14 @@ final class PlayerService {
|
||||
saveProgressAndSync()
|
||||
}
|
||||
|
||||
// Mirror this watch into the active account's server history. Only for
|
||||
// a genuinely new video: an audio-mode toggle or a quality switch
|
||||
// reloads the same one, and starting a session for those would post it
|
||||
// to the server a second time.
|
||||
if isNewVideo {
|
||||
watchHistorySyncService?.beginSession(video: video)
|
||||
}
|
||||
// Mirror this watch into the active account's server history.
|
||||
// Unconditional, and the service decides: `isNewVideo` is already false
|
||||
// for most openings, because openVideo() puts the video into state
|
||||
// before this task runs. The service compares against the session it is
|
||||
// holding instead, which keeps a reload of the same video — an
|
||||
// audio-mode toggle, a quality switch, a stream retry — from posting it
|
||||
// a second time.
|
||||
watchHistorySyncService?.beginSessionIfNeeded(video: video)
|
||||
|
||||
// Reset flag for the new video
|
||||
videoEndedNaturally = false
|
||||
@@ -660,14 +661,30 @@ final class PlayerService {
|
||||
} else if state.playbackState == .ended {
|
||||
// Restart from beginning when video has ended
|
||||
Task {
|
||||
await seek(to: 0)
|
||||
resume()
|
||||
await replayCurrentVideo()
|
||||
}
|
||||
} else {
|
||||
resume()
|
||||
}
|
||||
}
|
||||
|
||||
/// Plays the current video again from the beginning, as a new watch.
|
||||
///
|
||||
/// Every way a *finished* video is started again comes through here: the
|
||||
/// replay button, the end-of-queue overlay, repeat-one, play/pause on an
|
||||
/// ended video. Each of those is a second watch of the same video, so the
|
||||
/// mirroring session is started fresh and the video is posted to the
|
||||
/// account's history again — which is what moves it back to the front.
|
||||
/// Seeking to the start mid-video is not this, and does not pass here.
|
||||
func replayCurrentVideo() async {
|
||||
if let video = state.currentVideo {
|
||||
watchHistorySyncService?.beginSession(video: video)
|
||||
}
|
||||
|
||||
await seek(to: 0)
|
||||
resume()
|
||||
}
|
||||
|
||||
/// Stops playback and clears the player.
|
||||
func stop() {
|
||||
sleepPreventionService.allowSleep()
|
||||
@@ -807,9 +824,8 @@ final class PlayerService {
|
||||
// Handle queue mode behavior
|
||||
switch state.queueMode {
|
||||
case .repeatOne:
|
||||
// Restart current video
|
||||
await seek(to: 0)
|
||||
resume()
|
||||
// Restart current video — a second watch of it, not a continuation
|
||||
await replayCurrentVideo()
|
||||
return
|
||||
|
||||
case .repeatAll:
|
||||
|
||||
@@ -56,11 +56,25 @@ final class WatchHistorySyncService {
|
||||
|
||||
// MARK: - Session lifecycle
|
||||
|
||||
/// Starts a mirroring session for a video that is about to play.
|
||||
/// Starts a mirroring session unless this video is already the one being
|
||||
/// mirrored.
|
||||
///
|
||||
/// Called only for a genuinely new video. Reloads of the same one — the
|
||||
/// audio-mode toggle, a quality switch, a stream retry — must not reach
|
||||
/// here, or each of them would post again.
|
||||
/// What playback calls on every load, because a load is not by itself a new
|
||||
/// watch: an audio-mode toggle, a quality switch or a stream retry all come
|
||||
/// through the same path with the same video, and starting a session for
|
||||
/// each of them would post the video again every time. A session already
|
||||
/// standing for this video is left exactly as it is — including its
|
||||
/// `posted` flag, which is what makes the reload free.
|
||||
func beginSessionIfNeeded(video: Video) {
|
||||
if let currentSession, currentSession.videoID == video.id.videoID { return }
|
||||
beginSession(video: video)
|
||||
}
|
||||
|
||||
/// Starts a mirroring session for a video, replacing whatever was running.
|
||||
///
|
||||
/// For the moments that are a new watch of a video already on screen —
|
||||
/// replaying one that finished, repeat-one coming round again — where the
|
||||
/// point is precisely to post it a second time.
|
||||
func beginSession(video: Video) {
|
||||
currentSession = nil
|
||||
|
||||
@@ -130,6 +144,7 @@ final class WatchHistorySyncService {
|
||||
|
||||
do {
|
||||
try await accountService.markWatched(videoID: videoID, for: account)
|
||||
announceUpdate(videoID: videoID, accountID: accountID)
|
||||
return
|
||||
} catch is CancellationError {
|
||||
// The account was discarded or the app is going away. Not a
|
||||
@@ -144,7 +159,13 @@ final class WatchHistorySyncService {
|
||||
)
|
||||
return
|
||||
} catch {
|
||||
guard attempt == 0 else {
|
||||
// Only what a second attempt could plausibly answer differently:
|
||||
// a timeout, a dropped connection, a rate limit, a server that
|
||||
// fell over. A 400 for an ID the server will never accept is
|
||||
// just as wrong the second time.
|
||||
let isRetryable = (error as? APIError)?.isRetryable == true
|
||||
|
||||
guard attempt == 0, isRetryable else {
|
||||
LoggingService.shared.debug(
|
||||
"Watch history mirror dropped for \(videoID): \(error)",
|
||||
category: .api
|
||||
@@ -155,6 +176,23 @@ final class WatchHistorySyncService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Tells whoever is showing this account's server history that a video just
|
||||
/// reached the front of it.
|
||||
///
|
||||
/// The view has no other way to know: the local write it also does is not
|
||||
/// news about the server, and the account's history is only ever read by
|
||||
/// asking for it.
|
||||
private func announceUpdate(videoID: String, accountID: UUID) {
|
||||
NotificationCenter.default.post(
|
||||
name: .serverWatchHistoryDidUpdate,
|
||||
object: nil,
|
||||
userInfo: [
|
||||
ServerWatchHistoryUpdate.accountIDKey: accountID,
|
||||
ServerWatchHistoryUpdate.videoIDKey: videoID
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Draining
|
||||
|
||||
/// Waits for every post still in flight.
|
||||
@@ -189,3 +227,20 @@ final class WatchHistorySyncService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Announcement
|
||||
|
||||
extension Notification.Name {
|
||||
/// A video was recorded in an account's server-side history. The userInfo
|
||||
/// carries which account and which video, so a list showing another
|
||||
/// account's history can ignore it.
|
||||
static let serverWatchHistoryDidUpdate = Notification.Name("stream.yattee.serverWatchHistoryDidUpdate")
|
||||
}
|
||||
|
||||
/// The userInfo keys of ``Notification/Name/serverWatchHistoryDidUpdate``.
|
||||
enum ServerWatchHistoryUpdate {
|
||||
/// `UUID` — the account whose history gained the video.
|
||||
static let accountIDKey = "accountID"
|
||||
/// `String` — the video that was recorded.
|
||||
static let videoIDKey = "videoID"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user