-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added support for captions extraction.
- Loading branch information
Showing
13 changed files
with
389 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
41 changes: 41 additions & 0 deletions
41
Sources/YouTubeKit/BaseProtocols/Video/YouTubeVideo+getCaptions.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
// | ||
// YouTubeVideo+getCaptions.swift | ||
// | ||
// | ||
// Created by Antoine Bollengier on 27.06.2024. | ||
// Copyright © 2024 Antoine Bollengier (github.com/b5i). All rights reserved. | ||
// | ||
|
||
public extension YouTubeVideo { | ||
/// Get the captions for the current video. | ||
static func getCaptions(youtubeModel: YouTubeModel, captionType: YTCaption, result: @escaping @Sendable (Result<VideoCaptionsResponse, Error>) -> Void) { | ||
VideoCaptionsResponse.sendNonThrowingRequest(youtubeModel: youtubeModel, data: [.customURL: captionType.url.absoluteString], result: { response in | ||
switch response { | ||
case .success(let data): | ||
result(.success(data)) | ||
case .failure(let error): | ||
result(.failure(error)) | ||
} | ||
}) | ||
} | ||
|
||
/// Get the captions for the current video. | ||
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) | ||
static func getCaptionsThrowing(youtubeModel: YouTubeModel, captionType: YTCaption) async throws -> VideoCaptionsResponse { | ||
return try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation<VideoCaptionsResponse, Error>) in | ||
self.getCaptions(youtubeModel: youtubeModel, captionType: captionType, result: { result in | ||
continuation.resume(with: result) | ||
}) | ||
}) | ||
} | ||
|
||
/// Get the captions for the current video. | ||
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) | ||
static func getCaptions(youtubeModel: YouTubeModel, captionType: YTCaption) async -> Result<VideoCaptionsResponse, Error> { | ||
return await withCheckedContinuation({ (continuation: CheckedContinuation<Result<VideoCaptionsResponse, Error>, Never>) in | ||
self.getCaptions(youtubeModel: youtubeModel, captionType: captionType, result: { result in | ||
continuation.resume(returning: result) | ||
}) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
// | ||
// YTCaption.swift | ||
// | ||
// | ||
// Created by Antoine Bollengier on 27.06.2024. | ||
// Copyright © 2024 Antoine Bollengier (github.com/b5i). All rights reserved. | ||
// | ||
|
||
import Foundation | ||
|
||
public struct YTCaption: Sendable { | ||
public var languageCode: String | ||
|
||
public var languageName: String | ||
|
||
public var url: URL | ||
|
||
public var isTranslated: Bool | ||
|
||
public init(languageCode: String, languageName: String, url: URL, isTranslated: Bool) { | ||
self.languageCode = languageCode | ||
self.languageName = languageName | ||
self.url = url | ||
self.isTranslated = isTranslated | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
115 changes: 115 additions & 0 deletions
115
Sources/YouTubeKit/YouTubeResponseTypes/VideoInfos/VideoCaptionsResponse.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
// | ||
// VideoCaptionsResponse.swift | ||
// | ||
// | ||
// Created by Antoine Bollengier on 27.06.2024. | ||
// Copyright © 2024 Antoine Bollengier (github.com/b5i). All rights reserved. | ||
// | ||
|
||
import Foundation | ||
|
||
/// Struct representing a response containing the captions of a video. | ||
public struct VideoCaptionsResponse: YouTubeResponse { | ||
public static let headersType: HeaderTypes = .videoCaptionsHeaders | ||
|
||
public static let parametersValidationList: ValidationList = [.customURL: .urlValidator] | ||
|
||
public var captionParts: [CaptionPart] | ||
|
||
public init(captionParts: [CaptionPart]) { | ||
self.captionParts = captionParts | ||
} | ||
|
||
public static func decodeData(data: Data) throws -> VideoCaptionsResponse { | ||
var toReturn = VideoCaptionsResponse(captionParts: []) | ||
|
||
#if os(iOS) || os(tvOS) || os(watchOS) || os(macOS) || os(visionOS) | ||
let dataText = CFXMLCreateStringByUnescapingEntities(nil, CFXMLCreateStringByUnescapingEntities(nil, String(decoding: data, as: UTF8.self) as CFString, nil), nil) as String | ||
#else | ||
let dataText = String(decoding: data, as: UTF8.self) | ||
#endif | ||
|
||
let regexResults = dataText.ytkRegexMatches(for: #"(?:<text start=\"([0-9\.]*)\" dur=\"([0-9\.]*)">([\w\W]*?)<\/text>)"#) | ||
|
||
var currentEndTime: Double = Double.infinity | ||
|
||
for result in regexResults.reversed() { | ||
guard result.count == 4 else { continue } | ||
|
||
let startTime = Double(result[1]) ?? 0 | ||
let duration = min(Double(result[2]) ?? 0, currentEndTime - startTime) | ||
|
||
let text = result[3] | ||
|
||
toReturn.captionParts.append( | ||
CaptionPart( | ||
text: text, | ||
startTime: startTime, | ||
duration: duration | ||
) | ||
) | ||
|
||
currentEndTime = startTime | ||
} | ||
|
||
toReturn.captionParts.reverse() | ||
|
||
return toReturn | ||
} | ||
|
||
/// Decode json to give an instance of ``VideoInfosResponse``. | ||
/// - Parameter json: the json to be decoded. | ||
/// - Returns: an instance of ``VideoInfosResponse``. | ||
public static func decodeJSON(json: JSON) throws -> VideoCaptionsResponse { | ||
throw ResponseExtractionError(reponseType: Self.self, stepDescription: "Can't decode a VideoCaptionsResponse from some raw JSON.") | ||
} | ||
|
||
public func getFormattedString(withFormat format: CaptionFormats) -> String { | ||
func getTimeString(_ time: Double) -> String { | ||
let hours: String = String(format: "%02d", Int(time / 3600)) | ||
let minutes: String = String(format: "%02d", Int(time - (time / 3600).rounded(.down) * 3600) / 60) | ||
let seconds: String = String(format: "%02d", Int(time.truncatingRemainder(dividingBy: 60))) | ||
let milliseconds: String = String(format: "%03d", Int(time.truncatingRemainder(dividingBy: 1) * 1000)) | ||
|
||
return "\(hours):\(minutes):\(seconds)\(format == .vtt ? "." : ",")\(milliseconds)" | ||
} | ||
|
||
return """ | ||
\(format == .vtt ? "WEBVTT\n\n" : "")\( | ||
self.captionParts.enumerated() | ||
.map { offset, captionPart in | ||
return """ | ||
\(offset + 1) | ||
\(getTimeString(captionPart.startTime)) --> \(getTimeString(captionPart.startTime + captionPart.duration)) | ||
\(captionPart.text) | ||
""" | ||
} | ||
.joined(separator: "\n\n") | ||
) | ||
""" | ||
} | ||
|
||
public enum CaptionFormats { | ||
case vtt | ||
case srt | ||
} | ||
|
||
public struct CaptionPart: Sendable, Codable { | ||
/// Text of the caption. | ||
/// | ||
/// - Warning: The text might contain HTML entities (if `CFXMLCreateStringByUnescapingEntities` is not present), to remove them, call a function like `CFXMLCreateStringByUnescapingEntities()` two times on the text. | ||
public var text: String | ||
|
||
/// Start time of the caption, in seconds. | ||
public var startTime: Double | ||
|
||
/// Duration of the caption, in seconds. | ||
public var duration: Double | ||
|
||
public init(text: String, startTime: Double, duration: Double) { | ||
self.text = text | ||
self.startTime = startTime | ||
self.duration = duration | ||
} | ||
} | ||
} |
Oops, something went wrong.