Skip to content

Commit

Permalink
Added full support for comments.
Browse files Browse the repository at this point in the history
  • Loading branch information
b5i committed Jul 7, 2024
1 parent c001441 commit b5ad965
Show file tree
Hide file tree
Showing 29 changed files with 904 additions and 67 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public extension ContinuableResponse {
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
func fetchContinuationThrowing(youtubeModel: YouTubeModel, useCookies: Bool? = nil) async throws -> Continuation {
return try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation<Continuation, Error>) in
fetchContinuation(youtubeModel: youtubeModel, useCookies: useCookies, result: { response in
self.fetchContinuation(youtubeModel: youtubeModel, useCookies: useCookies, result: { response in
continuation.resume(with: response)
})
})
Expand Down
193 changes: 193 additions & 0 deletions Sources/YouTubeKit/BaseStructs/YTComment+actions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
//
// YTComment+actions.swift
//
//
// Created by Antoine Bollengier on 03.07.2024.
// Copyright © 2024 Antoine Bollengier (github.com/b5i). All rights reserved.
//

public extension YTComment {
/// Do one of the ``YTComment/CommentAction`` (like, dislike, removeLike, removeDislike, delete).
func commentAction(youtubeModel: YouTubeModel, action: YTComment.CommentAction, result: @escaping @Sendable (Error?) -> Void) {
switch action {
case .like:
guard let param = self.actionsParams[.like] else { result("self.actionsParams[.like] is not present."); return }
LikeCommentResponse.sendNonThrowingRequest(youtubeModel: youtubeModel, data: [.params: param], result: { res in
switch res {
case .success(_):
result(nil)
case .failure(let failure):
result(failure)
}
})
case .dislike:
guard let param = self.actionsParams[.dislike] else { result("self.actionsParams[.dislike] is not present."); return }
DislikeCommentResponse.sendNonThrowingRequest(youtubeModel: youtubeModel, data: [.params: param], result: { res in
switch res {
case .success(_):
result(nil)
case .failure(let failure):
result(failure)
}
})
case .removeLike:
guard let param = self.actionsParams[.removeLike] else { result("self.actionsParams[.removeLike] is not present."); return }
RemoveLikeCommentResponse.sendNonThrowingRequest(youtubeModel: youtubeModel, data: [.params: param], result: { res in
switch res {
case .success(_):
result(nil)
case .failure(let failure):
result(failure)
}
})
case .removeDislike:
guard let param = self.actionsParams[.removeDislike] else { result("self.actionsParams[.removeDislike] is not present."); return }
RemoveDislikeCommentResponse.sendNonThrowingRequest(youtubeModel: youtubeModel, data: [.params: param], result: { res in
switch res {
case .success(_):
result(nil)
case .failure(let failure):
result(failure)
}
})
case .delete:
guard let param = self.actionsParams[.delete] else { result("self.actionsParams[.delete] is not present."); return }
DeleteCommentResponse.sendNonThrowingRequest(youtubeModel: youtubeModel, data: [.params: param], result: { res in
switch res {
case .success(_):
result(nil)
case .failure(let failure):
result(failure)
}
})
case .edit:
result("edit is not supported via commentAction(_:_:_:), please use editComment(_:_:).")
case .reply:
result("reply is not supported via commentAction(_:_:_:), please use replyToComment(_:_:).")
case .repliesContinuation:
result("repliesContinuation is not supported via commentAction(_:_:_:), please use fetchRepliesContinuation(_:_:).")
case .translate:
result("translate is not supported via commentAction(_:_:_:), please use translateText(_:_:).")
}
}

@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
func commentAction(youtubeModel: YouTubeModel, action: YTComment.CommentAction) async throws {
return try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation<Void, Error>) in
self.commentAction(youtubeModel: youtubeModel, action: action, result: { error in
if let error = error {
continuation.resume(throwing: error)
} else {
continuation.resume()
}
})
})
}

/// Edit the text of a comment (the cookies from the used ``YouTubeModel`` must be the owner of the comment).
func editComment(withNewText text: String, youtubeModel: YouTubeModel, result: @escaping @Sendable (Error?) -> Void) {
guard let param = self.actionsParams[.edit] else { result("self.actionsParams[.edit] is not present."); return }
if (self.replyLevel ?? 0) == 0 {
EditCommentResponse.sendNonThrowingRequest(youtubeModel: youtubeModel, data: [.params: param, .text: text], result: { res in
switch res {
case .success(_):
result(nil)
case .failure(let failure):
result(failure)
}
})
} else {
EditReplyCommandResponse.sendNonThrowingRequest(youtubeModel: youtubeModel, data: [.params: param, .text: text], result: { res in
switch res {
case .success(_):
result(nil)
case .failure(let failure):
result(failure)
}
})
}
}

@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
/// Edit the text of a comment (the cookies from the used ``YouTubeModel`` must be the owner of the comment).
func editComment(withNewText text: String, youtubeModel: YouTubeModel) async throws {
return try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation<Void, Error>) in
self.editComment(withNewText: text, youtubeModel: youtubeModel, result: { error in
if let error = error {
continuation.resume(throwing: error)
} else {
continuation.resume()
}
})
})
}

/// Reply to a comment.
func replyToComment(youtubeModel: YouTubeModel, text: String, result: @escaping @Sendable (Result<ReplyCommentResponse, Error>) -> Void) {
guard let replyToken = self.actionsParams[.reply] else { result(.failure("self.actionsParams[.reply] is not present.")); return }
ReplyCommentResponse.sendNonThrowingRequest(youtubeModel: youtubeModel, data: [.params: replyToken, .text: text], result: { res in
switch res {
case .success(let success):
result(.success(success))
case .failure(let failure):
result(.failure(failure))
}
})
}

@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
/// Reply to a comment.
func replyToComment(youtubeModel: YouTubeModel, text: String) async throws -> ReplyCommentResponse {
return try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation<ReplyCommentResponse, Error>) in
self.replyToComment(youtubeModel: youtubeModel, text: text, result: { response in
continuation.resume(with: response)
})
})
}

/// Get the replies of a comment, can also be used to get the continuation of the replies.
func fetchRepliesContinuation(youtubeModel: YouTubeModel, useCookies: Bool? = nil, result: @escaping @Sendable (Result<VideoCommentsResponse.Continuation, Error>) -> Void) {
guard let repliesContinuationToken = self.actionsParams[.repliesContinuation] else { result(.failure("self.actionsParams[.repliesContinuation] is not present.")); return }
VideoCommentsResponse.Continuation.sendNonThrowingRequest(youtubeModel: youtubeModel, data: [.continuation: repliesContinuationToken], useCookies: useCookies, result: { res in
switch res {
case .success(let success):
result(.success(success))
case .failure(let failure):
result(.failure(failure))
}
})
}

@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
/// Get the replies of a comment, can also be used to get the continuation of the replies.
func fetchRepliesContinuation(youtubeModel: YouTubeModel, useCookies: Bool? = nil) async throws -> VideoCommentsResponse.Continuation {
return try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation<VideoCommentsResponse.Continuation, Error>) in
self.fetchRepliesContinuation(youtubeModel: youtubeModel, useCookies: useCookies, result: { response in
continuation.resume(with: response)
})
})
}

/// 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.
func translateText(youtubeModel: YouTubeModel, useCookies: Bool? = nil, result: @escaping @Sendable (Result<CommentTranslationResponse, Error>) -> Void) {
guard let translationToken = self.actionsParams[.translate] else { result(.failure("self.actionsParams[.translate] is not present.")); return }
CommentTranslationResponse.sendNonThrowingRequest(youtubeModel: youtubeModel, data: [.params: translationToken], useCookies: useCookies, result: { res in
switch res {
case .success(let success):
result(.success(success))
case .failure(let failure):
result(.failure(failure))
}
})
}

@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
/// 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.
func translateText(youtubeModel: YouTubeModel, useCookies: Bool? = nil) async throws -> CommentTranslationResponse {
return try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation<CommentTranslationResponse, Error>) in
self.translateText(youtubeModel: youtubeModel, useCookies: useCookies, result: { response in
continuation.resume(with: response)
})
})
}
}
12 changes: 12 additions & 0 deletions Sources/YouTubeKit/BaseStructs/YTPlaylist+fetchVideos.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,19 @@ public extension YTPlaylist {
})
}

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


/// Fetch the ``PlaylistInfosResponse`` related to the playlist.
@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
func fetchVideos(youtubeModel: YouTubeModel, useCookies: Bool? = nil, result: @escaping @Sendable (PlaylistInfosResponse?, Error?) -> Void) {
self.fetchVideos(youtubeModel: youtubeModel, useCookies: useCookies, result: { returning in
switch returning {
Expand All @@ -39,6 +50,7 @@ public extension YTPlaylist {

@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
/// Fetch the ``PlaylistInfosResponse`` related to the playlist.
@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
func fetchVideos(youtubeModel: YouTubeModel, useCookies: Bool? = nil) async -> (PlaylistInfosResponse?, Error?) {
do {
return await (try self.fetchVideosThrowing(youtubeModel: youtubeModel, useCookies: useCookies), nil)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ public extension ParameterValidator {
}
})

static let textSanitizerValidator = ParameterValidator(validator: { parameter in
if let parameter = parameter {
return .success(parameter.replacingOccurrences(of: "\\", with: #"\\"#).replacingOccurrences(of: "\"", with: #"\""#))
} else {
return .failure(.init(reason: "Parameter is nil.", validatorFailedNameDescriptor: "ExistenceValidator."))
}
})

static let channelIdValidator = ParameterValidator(validator: { channelId in
let validatorName = "ChannelId validator"
guard let channelId = channelId else { return .failure(.init(reason: "Nil value.", validatorFailedNameDescriptor: validatorName))}
Expand Down Expand Up @@ -77,7 +85,7 @@ public extension ParameterValidator {
}
})

static let urlValidator = ParameterValidator(needExistence: true, validator: { url in
static let urlValidator = ParameterValidator(validator: { url in
let validatorName = "URL validator"

guard let url = url else { return .failure(.init(reason: "Nil value.", validatorFailedNameDescriptor: validatorName)) } // should never be called because of the needExistence
Expand Down
10 changes: 8 additions & 2 deletions Sources/YouTubeKit/HeaderTypes+RawRepresentable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,16 @@ extension HeaderTypes: RawRepresentable {
return "removeLikeCommentHeaders"
case .removeDislikeCommentHeaders:
return "removeDislikeCommentHeaders"
case .updateCommentHeaders:
return "updateCommentHeaders"
case .editCommentHeaders:
return "editCommentHeaders"
case .replyCommentHeaders:
return "replyCommentHeaders"
case .removeCommentHeaders:
return "removeCommentHeaders"
case .translateCommentHeaders:
return "removeCommentHeaders"
case .editReplyCommentHeaders:
return "editReplyCommentHeaders"
case .customHeaders(let stringIdentifier):
return stringIdentifier
}
Expand Down
36 changes: 35 additions & 1 deletion Sources/YouTubeKit/HeaderTypes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -167,24 +167,58 @@ public enum HeaderTypes: Codable, Sendable {
/// - Parameter params: The operation param from ``TrendingVideosResponse/requestParams`` (optional).
case trendingVideosHeaders

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

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

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

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

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

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

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

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

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

/// Edit the text of a reply to a comment.
/// - Parameter params: the params from ``YTComment/actionsParams``[.edit] from the reply, only present if ``YouTubeModel/cookies`` contains valid cookies.
/// - Parameter text: the new text of the comment (no need to escape it).
case editReplyCommentHeaders

/// Delete a comment.
/// - Parameter params: the params from ``YTComment/actionsParams``[.delete], only present if ``YouTubeModel/cookies`` contains valid cookies.
case removeCommentHeaders

/// Translate the text of a comment.
/// - Parameter params: the params from ``YTComment/actionsParams``[.translate], only present if ``YouTubeModel/cookies`` contains valid cookies.
case translateCommentHeaders

/// For custom headers
case customHeaders(String)
}
3 changes: 2 additions & 1 deletion Sources/YouTubeKit/HeadersList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -155,12 +155,13 @@ public struct HeadersList: Codable {
case continuation
case params
case visitorData
case text

/// Those are used during the modification of a playlist
case movingVideoId
case videoBeforeId
case playlistEditToken

/// Used to completly replace the URL of the request, including the parameters that could potentially
case customURL
}
Expand Down
Loading

0 comments on commit b5ad965

Please sign in to comment.