Implement SponsorBlock API

This commit is contained in:
Arkadiusz Fal
2021-06-18 00:43:29 +02:00
parent 9d7abda63f
commit d551dee426
9 changed files with 190 additions and 29 deletions

View File

@@ -14,23 +14,34 @@ final class PlayerState: ObservableObject {
@Published private(set) var streamToLoad: Stream!
@Published private(set) var streamLoading = false
@Published private(set) var currentTime: CMTime?
@Published private(set) var savedTime: CMTime?
@Published var currentSegment: Segment?
var playerItem: AVPlayerItem {
let playerItem = AVPlayerItem(asset: composition)
playerItem.externalMetadata = [makeMetadataItem(.commonIdentifierTitle, value: video.title)]
playerItem.externalMetadata = [
makeMetadataItem(.commonIdentifierTitle, value: video.title),
makeMetadataItem(.quickTimeMetadataGenre, value: video.genre),
makeMetadataItem(.commonIdentifierDescription, value: video.description)
]
playerItem.preferredForwardBufferDuration = 10
return playerItem
}
var segmentsProvider: SponsorBlockSegmentsProvider
var timeObserver: Any?
init(_ video: Video) {
self.video = video
segmentsProvider = SponsorBlockSegmentsProvider(video.id)
segmentsProvider.load()
}
deinit {
print("destr deinit")
destroyPlayer()
}
@@ -51,12 +62,15 @@ final class PlayerState: ObservableObject {
func streamDidLoad(_ stream: Stream?) {
logger.info("didload stream: \(stream!.description)")
currentStream?.cancelLoadingAssets()
currentStream = stream
streamLoading = streamToLoad != stream
if streamToLoad == stream {
streamToLoad = nil
}
addTimeObserver()
}
func cancelLoadingStream(_ stream: Stream) {
@@ -121,6 +135,18 @@ final class PlayerState: ObservableObject {
player.cancelPendingPrerolls()
player.replaceCurrentItem(with: nil)
if timeObserver != nil {
player.removeTimeObserver(timeObserver!)
}
}
func addTimeObserver() {
let interval = CMTime(value: 1, timescale: 1)
timeObserver = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { time in
self.currentTime = time
self.currentSegment = self.segmentsProvider.segments.first { $0.timeInSegment(time) }
}
}
private func makeMetadataItem(_ identifier: AVMetadataIdentifier, value: Any) -> AVMetadataItem {

38
Model/Segment.swift Normal file
View File

@@ -0,0 +1,38 @@
import CoreMedia
import Foundation
import SwiftyJSON
// swiftlint:disable:next final_class
class Segment: ObservableObject, Hashable {
let category: String
let segment: [Double]
let uuid: String
let videoDuration: Int
init(category: String, segment: [Double], uuid: String, videoDuration: Int) {
self.category = category
self.segment = segment
self.uuid = uuid
self.videoDuration = videoDuration
}
func timeInSegment(_ time: CMTime) -> Bool {
(segment.first! ... segment.last!).contains(time.seconds)
}
var skipTo: CMTime {
CMTime(seconds: segment.last!, preferredTimescale: 1)
}
func hash(into hasher: inout Hasher) {
hasher.combine(uuid)
}
static func == (lhs: Segment, rhs: Segment) -> Bool {
lhs.uuid == rhs.uuid
}
func title() -> String {
category
}
}

View File

@@ -0,0 +1,24 @@
import Foundation
import SwiftyJSON
final class SponsorBlockSegment: Segment {
init(_ json: JSON) {
super.init(
category: json["category"].string!,
segment: json["segment"].array!.map { $0.double! },
uuid: json["UUID"].string!,
videoDuration: json["videoDuration"].int!
)
}
override func title() -> String {
switch category {
case "selfpromo":
return "self-promotion"
case "music_offtopic":
return "to music"
default:
return category
}
}
}

View File

@@ -0,0 +1,35 @@
import Alamofire
import Foundation
import SwiftyJSON
final class SponsorBlockSegmentsProvider: ObservableObject {
let categories = ["sponsor", "selfpromo", "outro", "intro", "music_offtopic", "interaction"]
@Published var video: Video?
@Published var segments = [Segment]()
var id: String
init(_ id: String) {
self.id = id
}
func load() {
AF.request("https://sponsor.ajay.app/api/skipSegments", parameters: parameters).responseJSON { response in
switch response.result {
case let .success(value):
self.segments = JSON(value).arrayValue.map { SponsorBlockSegment($0) }
case let .failure(error):
print(error)
}
}
}
private var parameters: [String: String] {
[
"videoID": id,
"categories": JSON(categories).rawString(String.Encoding.utf8)!
]
}
}

View File

@@ -13,6 +13,6 @@ final class TrendingCountriesProvider: DataProvider {
}
self.query = query
countries = Country.searchByName(query)
countries = Country.search(query)
}
}

View File

@@ -12,29 +12,11 @@ final class Video: Identifiable, ObservableObject {
var published: String
var views: Int
var channelID: String
var description: String
var genre: String
var streams = [Stream]()
init(
id: String,
title: String,
thumbnailURL: URL?,
author: String,
length: TimeInterval,
published: String,
views: Int,
channelID: String
) {
self.id = id
self.title = title
self.thumbnailURL = thumbnailURL
self.author = author
self.length = length
self.published = published
self.views = views
self.channelID = channelID
}
init(_ json: JSON) {
id = json["videoId"].stringValue
title = json["title"].stringValue
@@ -43,6 +25,9 @@ final class Video: Identifiable, ObservableObject {
published = json["publishedText"].stringValue
views = json["viewCount"].intValue
channelID = json["authorId"].stringValue
description = json["description"].stringValue
genre = json["genre"].stringValue
thumbnailURL = extractThumbnailURL(from: json)
streams = extractFormatStreams(from: json["formatStreams"].arrayValue)