Fix timed links resuming at watch position instead of URL timestamp

Timed links arrive wrapped as yattee://open?url=... from the share
extension, but the timestamp was parsed from the wrapper URL, where a
single-? form like youtu.be/ID?t=N hides t inside the url query item.
With no startTime the player fell back to saved watch progress.

- Add URLRouter.unwrapped(_:) resolving the wrapper to the inner URL
  (raw remainder after ?url=, since the share extension does not encode
  & and URLComponents would drop &t= parts) and use it in handleDeepLink
- Thread forceStartTime through openVideo/playPreferringDownloaded/play
  so explicit link timestamps beat the 90%-watched restart threshold,
  clamped to just before the end; resume flows keep the old behavior
- Carry the parsed timestamp through OpenLinkSheet's play action, which
  dropped it entirely
- Cover timestamp parsing and wrapper unwrapping in NavigationTests
This commit is contained in:
Arkadiusz Fal
2026-08-02 16:32:20 +02:00
parent b08974f4dc
commit d624a09335
5 changed files with 110 additions and 13 deletions

View File

@@ -96,6 +96,32 @@ struct URLRouter: Sendable {
return true
}
// MARK: - Wrapper Unwrapping
/// Resolve a `yattee://open?url={encoded_url}` wrapper (share extension) to the inner URL.
/// Returns the input unchanged for any other URL. The wrapper's query holds the full
/// original link, so timestamp parsing must run against the unwrapped URL.
///
/// The share extension encodes with `.urlQueryAllowed`, which leaves `?`, `&`, and `=`
/// intact - URLComponents would split the inner URL's own query into separate wrapper
/// items (losing e.g. `&t=120`), so take the raw remainder after `?url=` instead.
func unwrapped(_ url: URL) -> URL {
guard url.scheme?.lowercased() == "yattee", url.host == "open",
let range = url.absoluteString.range(of: "?url=") else {
return url
}
let raw = String(url.absoluteString[range.upperBound...])
guard let decoded = raw.removingPercentEncoding,
let innerURL = URL(string: decoded) else {
return url
}
// Unwrap once more in case the wrapper was itself wrapped
if innerURL.scheme?.lowercased() == "yattee", innerURL.host == "open", innerURL != url {
return unwrapped(innerURL)
}
return innerURL
}
// MARK: - Timestamp Parsing
/// Extract a timestamp (seconds) from a URL's query, supporting `t`, `time`, and `start`.