Skip to content

Commit 62ddbb0

Browse files
committed
add support for "already watched" time value on YTVideo
1 parent a10ff8d commit 62ddbb0

File tree

3 files changed

+52
-3
lines changed

3 files changed

+52
-3
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//
2+
// YTVideo+timeStringToSeconds.swift
3+
// YouTubeKit
4+
//
5+
// Created by Antoine Bollengier on 02.01.2026.
6+
// Copyright © 2026 Antoine Bollengier (github.com/b5i). All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
public extension YTVideo {
12+
/// Converts a time string (e.g. "1:30") to seconds.
13+
/// - Parameter timeString: The time string to convert.
14+
/// - Returns: The time in seconds, or nil if the format is invalid.
15+
static func timeStringToSeconds(_ timeString: String) -> Int? {
16+
let components = timeString.split(separator: ":").map(String.init)
17+
guard components.count <= 2 else { return nil }
18+
19+
let seconds = components.reversed().enumerated().reduce(0) { (total, element) in
20+
guard let value = Int(element.element) else { return total }
21+
return total + value * Int(pow(60, Double(element.offset)))
22+
}
23+
return seconds
24+
}
25+
}

Sources/YouTubeKit/BaseStructs/YTVideo.swift

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import Foundation
99

1010
/// Struct representing a video.
1111
public struct YTVideo: YTSearchResult, YouTubeVideo, Codable, Sendable {
12-
public init(id: Int? = nil, videoId: String, title: String? = nil, channel: YTLittleChannelInfos? = nil, viewCount: String? = nil, timePosted: String? = nil, timeLength: String? = nil, thumbnails: [YTThumbnail] = [], memberOnly: Bool? = nil) {
12+
public init(id: Int? = nil, videoId: String, title: String? = nil, channel: YTLittleChannelInfos? = nil, viewCount: String? = nil, timePosted: String? = nil, timeLength: String? = nil, thumbnails: [YTThumbnail] = [], memberOnly: Bool? = nil, startTime: Int? = nil) {
1313
self.id = id
1414
self.videoId = videoId
1515
self.title = title
@@ -19,10 +19,11 @@ public struct YTVideo: YTSearchResult, YouTubeVideo, Codable, Sendable {
1919
self.timeLength = timeLength
2020
self.thumbnails = thumbnails
2121
self.memberOnly = memberOnly
22+
self.startTime = startTime
2223
}
2324

2425
public static func == (lhs: YTVideo, rhs: YTVideo) -> Bool {
25-
return lhs.channel?.channelId == rhs.channel?.channelId && lhs.channel?.name == rhs.channel?.name && lhs.thumbnails == rhs.thumbnails && lhs.timeLength == rhs.timeLength && lhs.timePosted == rhs.timePosted && lhs.title == rhs.title && lhs.videoId == rhs.videoId && lhs.viewCount == rhs.viewCount && lhs.memberOnly == rhs.memberOnly
26+
return lhs.channel?.channelId == rhs.channel?.channelId && lhs.channel?.name == rhs.channel?.name && lhs.thumbnails == rhs.thumbnails && lhs.timeLength == rhs.timeLength && lhs.timePosted == rhs.timePosted && lhs.title == rhs.title && lhs.videoId == rhs.videoId && lhs.viewCount == rhs.viewCount && lhs.memberOnly == rhs.memberOnly && lhs.startTime == rhs.startTime
2627
}
2728

2829
public static func canBeDecoded(json: JSON) -> Bool {
@@ -87,6 +88,8 @@ public struct YTVideo: YTSearchResult, YouTubeVideo, Codable, Sendable {
8788

8889
YTThumbnail.appendThumbnails(json: json["thumbnail"], thumbnailList: &video.thumbnails)
8990

91+
video.startTime = Int(Double(json["thumbnailOverlays", 0, "thumbnailOverlayResumePlaybackRenderer", "percentDurationWatched"].intValue) * 0.01 * Double(video.timeLengthSeconds ?? 0).rounded(.down))
92+
9093
return video
9194
}
9295

@@ -123,7 +126,9 @@ public struct YTVideo: YTSearchResult, YouTubeVideo, Codable, Sendable {
123126

124127
YTThumbnail.appendThumbnails(json: json["contentImage", "thumbnailViewModel"], thumbnailList: &video.thumbnails)
125128

126-
video.timeLength = json["contentImage", "thumbnailViewModel", "overlays"].array?.first?["thumbnailOverlayBadgeViewModel", "thumbnailBadges"].array?.first?["thumbnailBadgeViewModel", "text"].string
129+
video.timeLength = json["contentImage", "thumbnailViewModel", "overlays", 0, "thumbnailOverlayBadgeViewModel", "thumbnailBadges", 0, "thumbnailBadgeViewModel", "text"].string ?? json["contentImage", "thumbnailViewModel", "overlays", 0, "thumbnailBottomOverlayViewModel", "badges", 0, "thumbnailBadgeViewModel", "text"].string
130+
131+
video.startTime = json["rendererContext", "commandContext", "onTap", "innertubeCommand", "watchEndpoint", "startTimeSeconds"].int
127132

128133
return video
129134
}
@@ -146,6 +151,9 @@ public struct YTVideo: YTSearchResult, YouTubeVideo, Codable, Sendable {
146151
/// A boolean inidicating whether the video is a member-only one. If it's true, you won't be able to request the streaming info of the video except if you provide the cookies of an account that's a member of the channel.
147152
public var memberOnly: Bool?
148153

154+
/// The start time of the video in seconds, represents the time that was already partially watched.
155+
public var startTime: Int?
156+
149157
/// Count of views of the video, in a shortened string.
150158
///
151159
/// Possibly not defined when reading in ``YTPlaylist/frontVideos`` properties.
@@ -163,6 +171,14 @@ public struct YTVideo: YTSearchResult, YouTubeVideo, Codable, Sendable {
163171
/// Can be `live` instead of `ab:cd` if the video is a livestream.
164172
public var timeLength: String?
165173

174+
/// Length in seconds of the video, extracted from ``timeLength``.
175+
public var timeLengthSeconds: Int? {
176+
if let timeLength {
177+
return Self.timeStringToSeconds(timeLength)
178+
}
179+
return nil
180+
}
181+
166182
/// Array of thumbnails.
167183
///
168184
/// Usually sorted by resolution, from low to high.

Sources/YouTubeKit/YouTubeResponseTypes/VideoInfos/VideoInfosResponse.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ public struct VideoInfosResponse: YouTubeResponse {
8989
/// The aspect ratio of the video (width/height).
9090
public var aspectRatio: Double?
9191

92+
/// The start time of the video in seconds, represents the time that was already partially watched or that the "t" parameter is set in the video URL. Currently disabled because we can't make VideoInfosResponse requests with cookies.
93+
///
94+
/// - Note: This property is also available on ``YTVideo/startTime``, please use this value.
95+
//public var startTime: Int? = nil
96+
9297
/// Array of formats used to download the video, they usually contain both audio and video data and the download speed is higher than the ``VideoInfosResponse/downloadFormats``.
9398
//@available(*, deprecated, message: "This property is unstable for the moment.")
9499
public var defaultFormats: [any DownloadFormat]
@@ -110,6 +115,7 @@ public struct VideoInfosResponse: YouTubeResponse {
110115
videoURLsExpireAt: Date? = nil,
111116
viewCount: String? = nil,
112117
aspectRatio: Double? = nil,
118+
//startTime: Int? = nil,
113119
defaultFormats: [any DownloadFormat] = [],
114120
downloadFormats: [any DownloadFormat] = []
115121
) {
@@ -125,6 +131,7 @@ public struct VideoInfosResponse: YouTubeResponse {
125131
self.videoURLsExpireAt = videoURLsExpireAt
126132
self.viewCount = viewCount
127133
self.aspectRatio = aspectRatio
134+
//self.startTime = startTime
128135
self.defaultFormats = defaultFormats
129136
self.downloadFormats = downloadFormats
130137
}
@@ -188,6 +195,7 @@ public struct VideoInfosResponse: YouTubeResponse {
188195
}(),
189196
viewCount: videoDetailsJSON["viewCount"].string,
190197
aspectRatio: streamingJSON["aspectRatio"].double,
198+
//startTime: json["playerConfig", "playbackStartConfig", "startSeconds"].int,
191199
defaultFormats: streamingJSON["formats"].arrayValue.compactMap { VideoInfosWithDownloadFormatsResponse.decodeFormatFromJSON(json: $0) },
192200
downloadFormats: streamingJSON["adaptiveFormats"].arrayValue.compactMap { VideoInfosWithDownloadFormatsResponse.decodeFormatFromJSON(json: $0) }
193201
)

0 commit comments

Comments
 (0)