mirror of
https://github.com/yattee/yattee.git
synced 2025-08-06 10:44:06 +00:00
Implement SponsorBlock API
This commit is contained in:
@@ -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
38
Model/Segment.swift
Normal 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
|
||||
}
|
||||
}
|
24
Model/SponsorBlockSegment.swift
Normal file
24
Model/SponsorBlockSegment.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
35
Model/SponsorBlockSegmentsProvider.swift
Normal file
35
Model/SponsorBlockSegmentsProvider.swift
Normal 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)!
|
||||
]
|
||||
}
|
||||
}
|
@@ -13,6 +13,6 @@ final class TrendingCountriesProvider: DataProvider {
|
||||
}
|
||||
|
||||
self.query = query
|
||||
countries = Country.searchByName(query)
|
||||
countries = Country.search(query)
|
||||
}
|
||||
}
|
||||
|
@@ -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)
|
||||
|
Reference in New Issue
Block a user