Skip to content

Commit b5ad965

Browse files
committed
Added full support for comments.
1 parent c001441 commit b5ad965

29 files changed

+904
-67
lines changed

Sources/YouTubeKit/BaseProtocols/Continuation/ContinuableResponse+fetchContinuation.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public extension ContinuableResponse {
2424
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
2525
func fetchContinuationThrowing(youtubeModel: YouTubeModel, useCookies: Bool? = nil) async throws -> Continuation {
2626
return try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation<Continuation, Error>) in
27-
fetchContinuation(youtubeModel: youtubeModel, useCookies: useCookies, result: { response in
27+
self.fetchContinuation(youtubeModel: youtubeModel, useCookies: useCookies, result: { response in
2828
continuation.resume(with: response)
2929
})
3030
})
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
//
2+
// YTComment+actions.swift
3+
//
4+
//
5+
// Created by Antoine Bollengier on 03.07.2024.
6+
// Copyright © 2024 Antoine Bollengier (github.com/b5i). All rights reserved.
7+
//
8+
9+
public extension YTComment {
10+
/// Do one of the ``YTComment/CommentAction`` (like, dislike, removeLike, removeDislike, delete).
11+
func commentAction(youtubeModel: YouTubeModel, action: YTComment.CommentAction, result: @escaping @Sendable (Error?) -> Void) {
12+
switch action {
13+
case .like:
14+
guard let param = self.actionsParams[.like] else { result("self.actionsParams[.like] is not present."); return }
15+
LikeCommentResponse.sendNonThrowingRequest(youtubeModel: youtubeModel, data: [.params: param], result: { res in
16+
switch res {
17+
case .success(_):
18+
result(nil)
19+
case .failure(let failure):
20+
result(failure)
21+
}
22+
})
23+
case .dislike:
24+
guard let param = self.actionsParams[.dislike] else { result("self.actionsParams[.dislike] is not present."); return }
25+
DislikeCommentResponse.sendNonThrowingRequest(youtubeModel: youtubeModel, data: [.params: param], result: { res in
26+
switch res {
27+
case .success(_):
28+
result(nil)
29+
case .failure(let failure):
30+
result(failure)
31+
}
32+
})
33+
case .removeLike:
34+
guard let param = self.actionsParams[.removeLike] else { result("self.actionsParams[.removeLike] is not present."); return }
35+
RemoveLikeCommentResponse.sendNonThrowingRequest(youtubeModel: youtubeModel, data: [.params: param], result: { res in
36+
switch res {
37+
case .success(_):
38+
result(nil)
39+
case .failure(let failure):
40+
result(failure)
41+
}
42+
})
43+
case .removeDislike:
44+
guard let param = self.actionsParams[.removeDislike] else { result("self.actionsParams[.removeDislike] is not present."); return }
45+
RemoveDislikeCommentResponse.sendNonThrowingRequest(youtubeModel: youtubeModel, data: [.params: param], result: { res in
46+
switch res {
47+
case .success(_):
48+
result(nil)
49+
case .failure(let failure):
50+
result(failure)
51+
}
52+
})
53+
case .delete:
54+
guard let param = self.actionsParams[.delete] else { result("self.actionsParams[.delete] is not present."); return }
55+
DeleteCommentResponse.sendNonThrowingRequest(youtubeModel: youtubeModel, data: [.params: param], result: { res in
56+
switch res {
57+
case .success(_):
58+
result(nil)
59+
case .failure(let failure):
60+
result(failure)
61+
}
62+
})
63+
case .edit:
64+
result("edit is not supported via commentAction(_:_:_:), please use editComment(_:_:).")
65+
case .reply:
66+
result("reply is not supported via commentAction(_:_:_:), please use replyToComment(_:_:).")
67+
case .repliesContinuation:
68+
result("repliesContinuation is not supported via commentAction(_:_:_:), please use fetchRepliesContinuation(_:_:).")
69+
case .translate:
70+
result("translate is not supported via commentAction(_:_:_:), please use translateText(_:_:).")
71+
}
72+
}
73+
74+
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
75+
func commentAction(youtubeModel: YouTubeModel, action: YTComment.CommentAction) async throws {
76+
return try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation<Void, Error>) in
77+
self.commentAction(youtubeModel: youtubeModel, action: action, result: { error in
78+
if let error = error {
79+
continuation.resume(throwing: error)
80+
} else {
81+
continuation.resume()
82+
}
83+
})
84+
})
85+
}
86+
87+
/// Edit the text of a comment (the cookies from the used ``YouTubeModel`` must be the owner of the comment).
88+
func editComment(withNewText text: String, youtubeModel: YouTubeModel, result: @escaping @Sendable (Error?) -> Void) {
89+
guard let param = self.actionsParams[.edit] else { result("self.actionsParams[.edit] is not present."); return }
90+
if (self.replyLevel ?? 0) == 0 {
91+
EditCommentResponse.sendNonThrowingRequest(youtubeModel: youtubeModel, data: [.params: param, .text: text], result: { res in
92+
switch res {
93+
case .success(_):
94+
result(nil)
95+
case .failure(let failure):
96+
result(failure)
97+
}
98+
})
99+
} else {
100+
EditReplyCommandResponse.sendNonThrowingRequest(youtubeModel: youtubeModel, data: [.params: param, .text: text], result: { res in
101+
switch res {
102+
case .success(_):
103+
result(nil)
104+
case .failure(let failure):
105+
result(failure)
106+
}
107+
})
108+
}
109+
}
110+
111+
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
112+
/// Edit the text of a comment (the cookies from the used ``YouTubeModel`` must be the owner of the comment).
113+
func editComment(withNewText text: String, youtubeModel: YouTubeModel) async throws {
114+
return try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation<Void, Error>) in
115+
self.editComment(withNewText: text, youtubeModel: youtubeModel, result: { error in
116+
if let error = error {
117+
continuation.resume(throwing: error)
118+
} else {
119+
continuation.resume()
120+
}
121+
})
122+
})
123+
}
124+
125+
/// Reply to a comment.
126+
func replyToComment(youtubeModel: YouTubeModel, text: String, result: @escaping @Sendable (Result<ReplyCommentResponse, Error>) -> Void) {
127+
guard let replyToken = self.actionsParams[.reply] else { result(.failure("self.actionsParams[.reply] is not present.")); return }
128+
ReplyCommentResponse.sendNonThrowingRequest(youtubeModel: youtubeModel, data: [.params: replyToken, .text: text], result: { res in
129+
switch res {
130+
case .success(let success):
131+
result(.success(success))
132+
case .failure(let failure):
133+
result(.failure(failure))
134+
}
135+
})
136+
}
137+
138+
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
139+
/// Reply to a comment.
140+
func replyToComment(youtubeModel: YouTubeModel, text: String) async throws -> ReplyCommentResponse {
141+
return try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation<ReplyCommentResponse, Error>) in
142+
self.replyToComment(youtubeModel: youtubeModel, text: text, result: { response in
143+
continuation.resume(with: response)
144+
})
145+
})
146+
}
147+
148+
/// Get the replies of a comment, can also be used to get the continuation of the replies.
149+
func fetchRepliesContinuation(youtubeModel: YouTubeModel, useCookies: Bool? = nil, result: @escaping @Sendable (Result<VideoCommentsResponse.Continuation, Error>) -> Void) {
150+
guard let repliesContinuationToken = self.actionsParams[.repliesContinuation] else { result(.failure("self.actionsParams[.repliesContinuation] is not present.")); return }
151+
VideoCommentsResponse.Continuation.sendNonThrowingRequest(youtubeModel: youtubeModel, data: [.continuation: repliesContinuationToken], useCookies: useCookies, result: { res in
152+
switch res {
153+
case .success(let success):
154+
result(.success(success))
155+
case .failure(let failure):
156+
result(.failure(failure))
157+
}
158+
})
159+
}
160+
161+
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
162+
/// Get the replies of a comment, can also be used to get the continuation of the replies.
163+
func fetchRepliesContinuation(youtubeModel: YouTubeModel, useCookies: Bool? = nil) async throws -> VideoCommentsResponse.Continuation {
164+
return try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation<VideoCommentsResponse.Continuation, Error>) in
165+
self.fetchRepliesContinuation(youtubeModel: youtubeModel, useCookies: useCookies, result: { response in
166+
continuation.resume(with: response)
167+
})
168+
})
169+
}
170+
171+
/// Translate the text of a comment, is available if YouTube thinks that the cookies' user or the language of the user agent (?) might need a translation.
172+
func translateText(youtubeModel: YouTubeModel, useCookies: Bool? = nil, result: @escaping @Sendable (Result<CommentTranslationResponse, Error>) -> Void) {
173+
guard let translationToken = self.actionsParams[.translate] else { result(.failure("self.actionsParams[.translate] is not present.")); return }
174+
CommentTranslationResponse.sendNonThrowingRequest(youtubeModel: youtubeModel, data: [.params: translationToken], useCookies: useCookies, result: { res in
175+
switch res {
176+
case .success(let success):
177+
result(.success(success))
178+
case .failure(let failure):
179+
result(.failure(failure))
180+
}
181+
})
182+
}
183+
184+
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
185+
/// Translate the text of a comment, is available if YouTube thinks that the cookies' user or the language of the user agent (?) might need a translation.
186+
func translateText(youtubeModel: YouTubeModel, useCookies: Bool? = nil) async throws -> CommentTranslationResponse {
187+
return try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation<CommentTranslationResponse, Error>) in
188+
self.translateText(youtubeModel: youtubeModel, useCookies: useCookies, result: { response in
189+
continuation.resume(with: response)
190+
})
191+
})
192+
}
193+
}

Sources/YouTubeKit/BaseStructs/YTPlaylist+fetchVideos.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,19 @@ public extension YTPlaylist {
2424
})
2525
}
2626

27+
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
28+
/// Fetch the ``PlaylistInfosResponse`` related to the playlist.
29+
func fetchVideos(youtubeModel: YouTubeModel, useCookies: Bool? = nil) async -> Result<PlaylistInfosResponse, Error> {
30+
do {
31+
return try await .success(self.fetchVideosThrowing(youtubeModel: youtubeModel, useCookies: useCookies))
32+
} catch {
33+
return .failure(error)
34+
}
35+
}
36+
2737

2838
/// Fetch the ``PlaylistInfosResponse`` related to the playlist.
39+
@available(*, deprecated, message: "This method will be removed in a future version of YouTubeKit, please use fetchInfos(youtubeModel: YouTubeModel, useCookies: Bool? = nil, result: @escaping (Result<PlaylistInfosResponse, Error>) -> ()) instead.") // safer and better to use the Result API instead of a tuple
2940
func fetchVideos(youtubeModel: YouTubeModel, useCookies: Bool? = nil, result: @escaping @Sendable (PlaylistInfosResponse?, Error?) -> Void) {
3041
self.fetchVideos(youtubeModel: youtubeModel, useCookies: useCookies, result: { returning in
3142
switch returning {
@@ -39,6 +50,7 @@ public extension YTPlaylist {
3950

4051
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
4152
/// Fetch the ``PlaylistInfosResponse`` related to the playlist.
53+
@available(*, deprecated, message: "This method will be removed in a future version of YouTubeKit, please use fetchInfos(youtubeModel: YouTubeModel, useCookies: Bool? = nil) -> Result<PlaylistInfosResponse, Error> instead.") // safer and better to use the Result API instead of a tuple
4254
func fetchVideos(youtubeModel: YouTubeModel, useCookies: Bool? = nil) async -> (PlaylistInfosResponse?, Error?) {
4355
do {
4456
return await (try self.fetchVideosThrowing(youtubeModel: youtubeModel, useCookies: useCookies), nil)

Sources/YouTubeKit/ErrorHandling/ParameterValidator+commonValidators.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ public extension ParameterValidator {
2929
}
3030
})
3131

32+
static let textSanitizerValidator = ParameterValidator(validator: { parameter in
33+
if let parameter = parameter {
34+
return .success(parameter.replacingOccurrences(of: "\\", with: #"\\"#).replacingOccurrences(of: "\"", with: #"\""#))
35+
} else {
36+
return .failure(.init(reason: "Parameter is nil.", validatorFailedNameDescriptor: "ExistenceValidator."))
37+
}
38+
})
39+
3240
static let channelIdValidator = ParameterValidator(validator: { channelId in
3341
let validatorName = "ChannelId validator"
3442
guard let channelId = channelId else { return .failure(.init(reason: "Nil value.", validatorFailedNameDescriptor: validatorName))}
@@ -77,7 +85,7 @@ public extension ParameterValidator {
7785
}
7886
})
7987

80-
static let urlValidator = ParameterValidator(needExistence: true, validator: { url in
88+
static let urlValidator = ParameterValidator(validator: { url in
8189
let validatorName = "URL validator"
8290

8391
guard let url = url else { return .failure(.init(reason: "Nil value.", validatorFailedNameDescriptor: validatorName)) } // should never be called because of the needExistence

Sources/YouTubeKit/HeaderTypes+RawRepresentable.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,16 @@ extension HeaderTypes: RawRepresentable {
103103
return "removeLikeCommentHeaders"
104104
case .removeDislikeCommentHeaders:
105105
return "removeDislikeCommentHeaders"
106-
case .updateCommentHeaders:
107-
return "updateCommentHeaders"
106+
case .editCommentHeaders:
107+
return "editCommentHeaders"
108108
case .replyCommentHeaders:
109109
return "replyCommentHeaders"
110+
case .removeCommentHeaders:
111+
return "removeCommentHeaders"
112+
case .translateCommentHeaders:
113+
return "removeCommentHeaders"
114+
case .editReplyCommentHeaders:
115+
return "editReplyCommentHeaders"
110116
case .customHeaders(let stringIdentifier):
111117
return stringIdentifier
112118
}

Sources/YouTubeKit/HeaderTypes.swift

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,24 +167,58 @@ public enum HeaderTypes: Codable, Sendable {
167167
/// - Parameter params: The operation param from ``TrendingVideosResponse/requestParams`` (optional).
168168
case trendingVideosHeaders
169169

170+
/// Get a video's comments.
171+
/// - Parameter continuation: The continuation token from ``MoreVideoInfosResponse/commentsContinuationToken``.
170172
case videoCommentsHeaders
171173

174+
/// Get a video's comments' continuation.
175+
/// - Parameter continuation: The continuation token from ``VideoCommentsResponse/continuationToken``.
172176
case videoCommentsContinuationHeaders
173177

178+
/// Create a comment.
179+
/// - Parameter params: the params from ``VideoCommentsResponse/commentCreationToken``
180+
/// - Parameter text: the text of the new comment (no need to escape it).
174181
case createCommentHeaders
175182

183+
/// Like a comment.
184+
/// - Parameter params: the params from ``YTComment/actionsParams``[.like], only present if ``YouTubeModel/cookies`` contains valid cookies.
176185
case likeCommentHeaders
177186

187+
/// Dislike a comment.
188+
/// - Parameter params: the params from ``YTComment/actionsParams``[.dislike], only present if ``YouTubeModel/cookies`` contains valid cookies.
178189
case dislikeCommentHeaders
179190

191+
/// Remove the like from a comment.
192+
/// - Parameter params: the params from ``YTComment/actionsParams``[.removeLike], only present if ``YouTubeModel/cookies`` contains valid cookies.
180193
case removeLikeCommentHeaders
181194

195+
/// Remove the dislike from a comment.
196+
/// - Parameter params: the params from ``YTComment/actionsParams``[.removeDislike], only present if ``YouTubeModel/cookies`` contains valid cookies.
182197
case removeDislikeCommentHeaders
183198

184-
case updateCommentHeaders
199+
/// Edit the content of a comment.
200+
/// - Parameter params: the params from ``YTComment/actionsParams``[.edit], only present if ``YouTubeModel/cookies`` contains valid cookies.
201+
/// - Parameter text: the new text of the comment (no need to escape it).
202+
case editCommentHeaders
185203

204+
/// Edit the content of a comment.
205+
/// - Parameter params: the params from ``YTComment/actionsParams``[.edit], only present if ``YouTubeModel/cookies`` contains valid cookies.
206+
/// - Parameter text: the new text of the comment (no need to escape it).
186207
case replyCommentHeaders
187208

209+
/// Edit the text of a reply to a comment.
210+
/// - Parameter params: the params from ``YTComment/actionsParams``[.edit] from the reply, only present if ``YouTubeModel/cookies`` contains valid cookies.
211+
/// - Parameter text: the new text of the comment (no need to escape it).
212+
case editReplyCommentHeaders
213+
214+
/// Delete a comment.
215+
/// - Parameter params: the params from ``YTComment/actionsParams``[.delete], only present if ``YouTubeModel/cookies`` contains valid cookies.
216+
case removeCommentHeaders
217+
218+
/// Translate the text of a comment.
219+
/// - Parameter params: the params from ``YTComment/actionsParams``[.translate], only present if ``YouTubeModel/cookies`` contains valid cookies.
220+
case translateCommentHeaders
221+
188222
/// For custom headers
189223
case customHeaders(String)
190224
}

Sources/YouTubeKit/HeadersList.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,12 +155,13 @@ public struct HeadersList: Codable {
155155
case continuation
156156
case params
157157
case visitorData
158+
case text
158159

159160
/// Those are used during the modification of a playlist
160161
case movingVideoId
161162
case videoBeforeId
162163
case playlistEditToken
163-
164+
164165
/// Used to completly replace the URL of the request, including the parameters that could potentially
165166
case customURL
166167
}

0 commit comments

Comments
 (0)