mirror of
				https://github.com/yattee/yattee.git
				synced 2025-11-03 22:22:02 +00:00 
			
		
		
		
	Implement SponsorBlock API
This commit is contained in:
		@@ -117,6 +117,14 @@ struct PlayerViewController: UIViewControllerRepresentable {
 | 
			
		||||
        #if os(tvOS)
 | 
			
		||||
            controller.transportBarCustomMenuItems = items
 | 
			
		||||
        #endif
 | 
			
		||||
 | 
			
		||||
        if let skip = skipSegmentAction {
 | 
			
		||||
            if controller.contextualActions.isEmpty {
 | 
			
		||||
                controller.contextualActions = [skip]
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            controller.contextualActions = []
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fileprivate var streamingQualityMenu: UIMenu {
 | 
			
		||||
@@ -149,4 +157,16 @@ struct PlayerViewController: UIViewControllerRepresentable {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private var skipSegmentAction: UIAction? {
 | 
			
		||||
        if state.currentSegment == nil {
 | 
			
		||||
            return nil
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return UIAction(title: "Skip \(state.currentSegment!.title())") { _ in
 | 
			
		||||
            DispatchQueue.main.async {
 | 
			
		||||
                state.player.seek(to: state.currentSegment!.skipTo)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -82,6 +82,9 @@
 | 
			
		||||
		37B767DC2677C3CA0098BAA8 /* PlayerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B767DA2677C3CA0098BAA8 /* PlayerState.swift */; };
 | 
			
		||||
		37B767DD2677C3CA0098BAA8 /* PlayerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B767DA2677C3CA0098BAA8 /* PlayerState.swift */; };
 | 
			
		||||
		37B767E02678C5BF0098BAA8 /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = 37B767DF2678C5BF0098BAA8 /* Logging */; };
 | 
			
		||||
		37C7A1D5267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; };
 | 
			
		||||
		37C7A1D6267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; };
 | 
			
		||||
		37C7A1D7267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; };
 | 
			
		||||
		37C7A9042679059200E721B4 /* AVKeyValueStatus+String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A9032679059200E721B4 /* AVKeyValueStatus+String.swift */; };
 | 
			
		||||
		37C7A905267905AE00E721B4 /* AVKeyValueStatus+String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A9032679059200E721B4 /* AVKeyValueStatus+String.swift */; };
 | 
			
		||||
		37C7A906267905AF00E721B4 /* AVKeyValueStatus+String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A9032679059200E721B4 /* AVKeyValueStatus+String.swift */; };
 | 
			
		||||
@@ -128,6 +131,12 @@
 | 
			
		||||
		37D4B1B42672A30700C925CA /* VideoDetailsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D4B1B32672A30700C925CA /* VideoDetailsProvider.swift */; };
 | 
			
		||||
		37D4B1B52672A30700C925CA /* VideoDetailsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D4B1B32672A30700C925CA /* VideoDetailsProvider.swift */; };
 | 
			
		||||
		37D4B1B62672A30700C925CA /* VideoDetailsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D4B1B32672A30700C925CA /* VideoDetailsProvider.swift */; };
 | 
			
		||||
		37EAD86B267B9C5600D9E01B /* SponsorBlockSegmentsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EAD86A267B9C5600D9E01B /* SponsorBlockSegmentsProvider.swift */; };
 | 
			
		||||
		37EAD86C267B9C5600D9E01B /* SponsorBlockSegmentsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EAD86A267B9C5600D9E01B /* SponsorBlockSegmentsProvider.swift */; };
 | 
			
		||||
		37EAD86D267B9C5600D9E01B /* SponsorBlockSegmentsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EAD86A267B9C5600D9E01B /* SponsorBlockSegmentsProvider.swift */; };
 | 
			
		||||
		37EAD86F267B9ED100D9E01B /* Segment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EAD86E267B9ED100D9E01B /* Segment.swift */; };
 | 
			
		||||
		37EAD870267B9ED100D9E01B /* Segment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EAD86E267B9ED100D9E01B /* Segment.swift */; };
 | 
			
		||||
		37EAD871267B9ED100D9E01B /* Segment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EAD86E267B9ED100D9E01B /* Segment.swift */; };
 | 
			
		||||
/* End PBXBuildFile section */
 | 
			
		||||
 | 
			
		||||
/* Begin PBXContainerItemProxy section */
 | 
			
		||||
@@ -177,6 +186,7 @@
 | 
			
		||||
		37AAF29B26741B5F007FC770 /* SubscriptionVideosProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionVideosProvider.swift; sourceTree = "<group>"; };
 | 
			
		||||
		37AAF29F26741C97007FC770 /* SubscriptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionsView.swift; sourceTree = "<group>"; };
 | 
			
		||||
		37B767DA2677C3CA0098BAA8 /* PlayerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerState.swift; sourceTree = "<group>"; };
 | 
			
		||||
		37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockSegment.swift; sourceTree = "<group>"; };
 | 
			
		||||
		37C7A9032679059200E721B4 /* AVKeyValueStatus+String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVKeyValueStatus+String.swift"; sourceTree = "<group>"; };
 | 
			
		||||
		37CEE4B42677B628005A1EFE /* StreamType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamType.swift; sourceTree = "<group>"; };
 | 
			
		||||
		37CEE4B82677B63F005A1EFE /* StreamResolution.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamResolution.swift; sourceTree = "<group>"; };
 | 
			
		||||
@@ -202,6 +212,8 @@
 | 
			
		||||
		37D4B1AE26729DEB00C925CA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
 | 
			
		||||
		37D4B1AF2672A01000C925CA /* DataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataProvider.swift; sourceTree = "<group>"; };
 | 
			
		||||
		37D4B1B32672A30700C925CA /* VideoDetailsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDetailsProvider.swift; sourceTree = "<group>"; };
 | 
			
		||||
		37EAD86A267B9C5600D9E01B /* SponsorBlockSegmentsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockSegmentsProvider.swift; sourceTree = "<group>"; };
 | 
			
		||||
		37EAD86E267B9ED100D9E01B /* Segment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Segment.swift; sourceTree = "<group>"; };
 | 
			
		||||
/* End PBXFileReference section */
 | 
			
		||||
 | 
			
		||||
/* Begin PBXFrameworksBuildPhase section */
 | 
			
		||||
@@ -376,16 +388,19 @@
 | 
			
		||||
				37B767DA2677C3CA0098BAA8 /* PlayerState.swift */,
 | 
			
		||||
				37D4B19226717CE100C925CA /* PopularVideosProvider.swift */,
 | 
			
		||||
				37AAF2812673791F007FC770 /* SearchedVideosProvider.swift */,
 | 
			
		||||
				37EAD86E267B9ED100D9E01B /* Segment.swift */,
 | 
			
		||||
				37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */,
 | 
			
		||||
				37EAD86A267B9C5600D9E01B /* SponsorBlockSegmentsProvider.swift */,
 | 
			
		||||
				37CEE4C02677B697005A1EFE /* Stream.swift */,
 | 
			
		||||
				37CEE4B82677B63F005A1EFE /* StreamResolution.swift */,
 | 
			
		||||
				37CEE4B42677B628005A1EFE /* StreamType.swift */,
 | 
			
		||||
				37AAF29B26741B5F007FC770 /* SubscriptionVideosProvider.swift */,
 | 
			
		||||
				3705B181267B4E4900704544 /* TrendingCategory.swift */,
 | 
			
		||||
				3714167A267AA1CF006CA35D /* TrendingCountriesProvider.swift */,
 | 
			
		||||
				37141676267A9AAD006CA35D /* TrendingState.swift */,
 | 
			
		||||
				3714167E267AB55D006CA35D /* TrendingVideosProvider.swift */,
 | 
			
		||||
				37D4B19626717E1500C925CA /* Video.swift */,
 | 
			
		||||
				37D4B1B32672A30700C925CA /* VideoDetailsProvider.swift */,
 | 
			
		||||
				3714167E267AB55D006CA35D /* TrendingVideosProvider.swift */,
 | 
			
		||||
				3705B181267B4E4900704544 /* TrendingCategory.swift */,
 | 
			
		||||
			);
 | 
			
		||||
			path = Model;
 | 
			
		||||
			sourceTree = "<group>";
 | 
			
		||||
@@ -639,6 +654,7 @@
 | 
			
		||||
				37D4B19326717CE100C925CA /* PopularVideosProvider.swift in Sources */,
 | 
			
		||||
				37AAF29C26741B5F007FC770 /* SubscriptionVideosProvider.swift in Sources */,
 | 
			
		||||
				37141668267A83F9006CA35D /* StreamAVPlayerViewController.swift in Sources */,
 | 
			
		||||
				37EAD86B267B9C5600D9E01B /* SponsorBlockSegmentsProvider.swift in Sources */,
 | 
			
		||||
				377FC7E6267A085600A6BBAF /* PlayerView.swift in Sources */,
 | 
			
		||||
				37CEE4C12677B697005A1EFE /* Stream.swift in Sources */,
 | 
			
		||||
				37141677267A9AAD006CA35D /* TrendingState.swift in Sources */,
 | 
			
		||||
@@ -646,6 +662,7 @@
 | 
			
		||||
				377FC7DC267A081800A6BBAF /* PopularVideosView.swift in Sources */,
 | 
			
		||||
				3714167F267AB55D006CA35D /* TrendingVideosProvider.swift in Sources */,
 | 
			
		||||
				3705B182267B4E4900704544 /* TrendingCategory.swift in Sources */,
 | 
			
		||||
				37EAD86F267B9ED100D9E01B /* Segment.swift in Sources */,
 | 
			
		||||
				37CEE4B52677B628005A1EFE /* StreamType.swift in Sources */,
 | 
			
		||||
				3714166F267A8ACC006CA35D /* TrendingView.swift in Sources */,
 | 
			
		||||
				377FC7E3267A084A00A6BBAF /* VideoThumbnailView.swift in Sources */,
 | 
			
		||||
@@ -657,6 +674,7 @@
 | 
			
		||||
				377FC7E9267A085D00A6BBAF /* PlayerViewController.swift in Sources */,
 | 
			
		||||
				377FC7E5267A084E00A6BBAF /* SearchView.swift in Sources */,
 | 
			
		||||
				377FC7E1267A082600A6BBAF /* ChannelView.swift in Sources */,
 | 
			
		||||
				37C7A1D5267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */,
 | 
			
		||||
				37C7A9042679059200E721B4 /* AVKeyValueStatus+String.swift in Sources */,
 | 
			
		||||
				37B767DB2677C3CA0098BAA8 /* PlayerState.swift in Sources */,
 | 
			
		||||
				37D4B1B42672A30700C925CA /* VideoDetailsProvider.swift in Sources */,
 | 
			
		||||
@@ -678,6 +696,7 @@
 | 
			
		||||
				37D4B19426717CE100C925CA /* PopularVideosProvider.swift in Sources */,
 | 
			
		||||
				37AAF29D26741B5F007FC770 /* SubscriptionVideosProvider.swift in Sources */,
 | 
			
		||||
				37141669267A83F9006CA35D /* StreamAVPlayerViewController.swift in Sources */,
 | 
			
		||||
				37EAD86C267B9C5600D9E01B /* SponsorBlockSegmentsProvider.swift in Sources */,
 | 
			
		||||
				377FC7E7267A085600A6BBAF /* PlayerView.swift in Sources */,
 | 
			
		||||
				37CEE4C22677B697005A1EFE /* Stream.swift in Sources */,
 | 
			
		||||
				37141678267A9AAD006CA35D /* TrendingState.swift in Sources */,
 | 
			
		||||
@@ -685,6 +704,7 @@
 | 
			
		||||
				377FC7DD267A081A00A6BBAF /* PopularVideosView.swift in Sources */,
 | 
			
		||||
				37141680267AB55D006CA35D /* TrendingVideosProvider.swift in Sources */,
 | 
			
		||||
				3705B183267B4E4900704544 /* TrendingCategory.swift in Sources */,
 | 
			
		||||
				37EAD870267B9ED100D9E01B /* Segment.swift in Sources */,
 | 
			
		||||
				37CEE4B62677B628005A1EFE /* StreamType.swift in Sources */,
 | 
			
		||||
				37141670267A8ACC006CA35D /* TrendingView.swift in Sources */,
 | 
			
		||||
				377FC7E2267A084A00A6BBAF /* VideoThumbnailView.swift in Sources */,
 | 
			
		||||
@@ -696,6 +716,7 @@
 | 
			
		||||
				377FC7E8267A085D00A6BBAF /* PlayerViewController.swift in Sources */,
 | 
			
		||||
				377FC7E4267A084E00A6BBAF /* SearchView.swift in Sources */,
 | 
			
		||||
				377FC7E0267A082600A6BBAF /* ChannelView.swift in Sources */,
 | 
			
		||||
				37C7A1D6267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */,
 | 
			
		||||
				37C7A906267905AF00E721B4 /* AVKeyValueStatus+String.swift in Sources */,
 | 
			
		||||
				37B767DC2677C3CA0098BAA8 /* PlayerState.swift in Sources */,
 | 
			
		||||
				37D4B1B52672A30700C925CA /* VideoDetailsProvider.swift in Sources */,
 | 
			
		||||
@@ -730,6 +751,7 @@
 | 
			
		||||
			buildActionMask = 2147483647;
 | 
			
		||||
			files = (
 | 
			
		||||
				37AAF28026737550007FC770 /* SearchView.swift in Sources */,
 | 
			
		||||
				37EAD871267B9ED100D9E01B /* Segment.swift in Sources */,
 | 
			
		||||
				37CEE4BF2677B670005A1EFE /* AudioVideoStream.swift in Sources */,
 | 
			
		||||
				37CEE4B72677B628005A1EFE /* StreamType.swift in Sources */,
 | 
			
		||||
				3714166A267A83F9006CA35D /* StreamAVPlayerViewController.swift in Sources */,
 | 
			
		||||
@@ -742,6 +764,7 @@
 | 
			
		||||
				37D4B1B22672A01000C925CA /* DataProvider.swift in Sources */,
 | 
			
		||||
				37141671267A8ACC006CA35D /* TrendingView.swift in Sources */,
 | 
			
		||||
				37AAF29226740715007FC770 /* AppState.swift in Sources */,
 | 
			
		||||
				37EAD86D267B9C5600D9E01B /* SponsorBlockSegmentsProvider.swift in Sources */,
 | 
			
		||||
				3705B17C267B4D9A00704544 /* VisualEffectView.swift in Sources */,
 | 
			
		||||
				3741B5302676213400125C5E /* PlayerViewController.swift in Sources */,
 | 
			
		||||
				37B767DD2677C3CA0098BAA8 /* PlayerState.swift in Sources */,
 | 
			
		||||
@@ -751,6 +774,7 @@
 | 
			
		||||
				37AAF27E26737323007FC770 /* PopularVideosView.swift in Sources */,
 | 
			
		||||
				37AAF29A26740A01007FC770 /* VideosView.swift in Sources */,
 | 
			
		||||
				37AAF2962674086B007FC770 /* TabSelection.swift in Sources */,
 | 
			
		||||
				37C7A1D7267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */,
 | 
			
		||||
				37CEE4C32677B697005A1EFE /* Stream.swift in Sources */,
 | 
			
		||||
				37AAF28A2673AB89007FC770 /* ChannelView.swift in Sources */,
 | 
			
		||||
				37AAF28E2673ABD3007FC770 /* ChannelVideosProvider.swift in Sources */,
 | 
			
		||||
 
 | 
			
		||||
@@ -521,10 +521,18 @@ extension Country {
 | 
			
		||||
        return result
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static func searchByName(_ name: String) -> [Country] {
 | 
			
		||||
        let countries = filteredCountries { stringFolding($0) == stringFolding(name) }
 | 
			
		||||
    static func search(_ query: String) -> [Country] {
 | 
			
		||||
        if let country = searchByCode(query) {
 | 
			
		||||
            return [country]
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return countries.isEmpty ? searchByPartialName(name) : countries
 | 
			
		||||
        let countries = filteredCountries { stringFolding($0) == stringFolding(query) }
 | 
			
		||||
 | 
			
		||||
        return countries.isEmpty ? searchByPartialName(query) : countries
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static func searchByCode(_ code: String) -> Country? {
 | 
			
		||||
        Country(rawValue: code.uppercased())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static func searchByPartialName(_ name: String) -> [Country] {
 | 
			
		||||
@@ -540,7 +548,8 @@ extension Country {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static func filteredCountries(_ predicate: (String) -> Bool) -> [Country] {
 | 
			
		||||
        Country.allCases.map { $0.name }
 | 
			
		||||
        Country.allCases
 | 
			
		||||
            .map { $0.name }
 | 
			
		||||
            .filter(predicate)
 | 
			
		||||
            .compactMap { string in Country.allCases.first { $0.name == string } }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user