diff --git a/Model/Accounts/AccountValidator.swift b/Model/Accounts/AccountValidator.swift index 2429a051..4a428537 100644 --- a/Model/Accounts/AccountValidator.swift +++ b/Model/Accounts/AccountValidator.swift @@ -55,9 +55,6 @@ final class AccountValidator: Service { case .piped: return resource("/streams/dQw4w9WgXcQ") - - case .demoApp: - return resource("/") } } diff --git a/Model/Accounts/AccountsModel.swift b/Model/Accounts/AccountsModel.swift index afd473aa..6b2d3c09 100644 --- a/Model/Accounts/AccountsModel.swift +++ b/Model/Accounts/AccountsModel.swift @@ -7,7 +7,6 @@ final class AccountsModel: ObservableObject { @Published private var invidious = InvidiousAPI() @Published private var piped = PipedAPI() - @Published private var demo = DemoAppAPI() @Published var publicAccount: Account? @@ -39,8 +38,6 @@ final class AccountsModel: ObservableObject { return piped case .invidious: return invidious - case .demoApp: - return demo } } @@ -53,7 +50,7 @@ final class AccountsModel: ObservableObject { } var isDemo: Bool { - current?.app == .demoApp + false } init() { @@ -91,8 +88,6 @@ final class AccountsModel: ObservableObject { invidious.setAccount(account) case .piped: piped.setAccount(account) - case .demoApp: - break } Defaults[.lastAccountIsPublic] = account.isPublic diff --git a/Model/Accounts/Instance.swift b/Model/Accounts/Instance.swift index 2b5be30d..4275f134 100644 --- a/Model/Accounts/Instance.swift +++ b/Model/Accounts/Instance.swift @@ -26,8 +26,6 @@ struct Instance: Defaults.Serializable, Hashable, Identifiable { return InvidiousAPI(account: anonymousAccount) case .piped: return PipedAPI(account: anonymousAccount) - case .demoApp: - return DemoAppAPI() } } @@ -36,9 +34,7 @@ struct Instance: Defaults.Serializable, Hashable, Identifiable { } var longDescription: String { - guard app != .demoApp else { return "Demo" } - - return name.isEmpty ? "\(app.name) - \(apiURL)" : "\(app.name) - \(name) (\(apiURL))" + name.isEmpty ? "\(app.name) - \(apiURL)" : "\(app.name) - \(name) (\(apiURL))" } var shortDescription: String { diff --git a/Model/Applications/DemoAppAPI.swift b/Model/Applications/DemoAppAPI.swift deleted file mode 100644 index b6547b52..00000000 --- a/Model/Applications/DemoAppAPI.swift +++ /dev/null @@ -1,395 +0,0 @@ -import AVFoundation -import Foundation -import Siesta -import SwiftyJSON - -final class DemoAppAPI: Service, ObservableObject, VideosAPI { - static var url = "https://r.yattee.stream/demo" - - var account: Account! { - .init( - id: UUID().uuidString, - app: .demoApp, - name: "Demo", - url: Self.url, - anonymous: true - ) - } - - var signedIn: Bool { - true - } - - init() { - super.init() - - configure() - } - - func configure() { - configure { - $0.pipeline[.parsing].add(SwiftyJSONTransformer, contentTypes: ["*/json"]) - } - - configureTransformer(pathPattern("channels/*"), requestMethods: [.get]) { (content: Entity) -> Channel? in - self.extractChannel(from: content.json) - } - - configureTransformer(pathPattern("search*"), requestMethods: [.get]) { (content: Entity) -> SearchPage in - let nextPage = content.json.dictionaryValue["nextpage"]?.string - return SearchPage( - results: self.extractContentItems(from: content.json.dictionaryValue["items"]!), - nextPage: nextPage, - last: nextPage == "null" - ) - } - - configureTransformer(pathPattern("suggestions*")) { (content: Entity) -> [String] in - content.json.arrayValue.map(String.init) - } - - configureTransformer(pathPattern("videos/*")) { (content: Entity) -> Video? in - self.extractVideo(from: content.json) - } - - configureTransformer(pathPattern("trending*")) { (content: Entity) -> [Video] in - self.extractVideos(from: content.json) - } - } - - func channel(_ channel: String) -> Resource { - resource(baseURL: Self.url, path: "/channels/\(channel).json") - } - - func channelByName(_: String) -> Resource? { - resource(baseURL: Self.url, path: "") - } - - func channelByUsername(_: String) -> Resource? { - resource(baseURL: Self.url, path: "") - } - - func channelVideos(_ id: String) -> Resource { - resource(baseURL: Self.url, path: "/channels/\(id).json") - } - - func trending(country _: Country, category _: TrendingCategory?) -> Resource { - resource(baseURL: Self.url, path: "/trending.json") - } - - func search(_ query: SearchQuery, page: String?) -> Resource { - resource(baseURL: Self.url, path: "/search.json") - .withParam("q", query.query) - .withParam("p", page) - } - - func searchSuggestions(query _: String) -> Resource { - resource(baseURL: Self.url, path: "/suggestions.json") - } - - func video(_ id: Video.ID) -> Resource { - resource(baseURL: Self.url, path: "/videos/\(id).json") - } - - var subscriptions: Resource? - - var feed: Resource? - - var home: Resource? - - var popular: Resource? - - var playlists: Resource? - - func subscribe(_: String, onCompletion _: @escaping () -> Void) {} - - func unsubscribe(_: String, onCompletion _: @escaping () -> Void) {} - - func playlist(_: String) -> Resource? { - resource(baseURL: Self.url, path: "") - } - - func playlistVideo(_: String, _: String) -> Resource? { - resource(baseURL: Self.url, path: "") - } - - func playlistVideos(_: String) -> Resource? { - resource(baseURL: Self.url, path: "") - } - - func addVideoToPlaylist(_: String, _: String, onFailure _: @escaping (RequestError) -> Void, onSuccess _: @escaping () -> Void) {} - - func removeVideoFromPlaylist(_: String, _: String, onFailure _: @escaping (RequestError) -> Void, onSuccess _: @escaping () -> Void) {} - - func playlistForm(_: String, _: String, playlist _: Playlist?, onFailure _: @escaping (RequestError) -> Void, onSuccess _: @escaping (Playlist?) -> Void) {} - - func deletePlaylist(_: Playlist, onFailure _: @escaping (RequestError) -> Void, onSuccess _: @escaping () -> Void) {} - - func channelPlaylist(_: String) -> Resource? { - resource(baseURL: Self.url, path: "") - } - - func comments(_: Video.ID, page _: String?) -> Resource? { - resource(baseURL: Self.url, path: "") - } - - private func pathPattern(_ path: String) -> String { - "**\(Self.url)/\(path)" - } - - private func extractChannel(from content: JSON) -> Channel? { - let attributes = content.dictionaryValue - guard let id = attributes["id"]?.string ?? - (attributes["url"] ?? attributes["uploaderUrl"])?.string?.components(separatedBy: "/").last - else { - return nil - } - - let subscriptionsCount = attributes["subscriberCount"]?.int ?? attributes["subscribers"]?.int - - var videos = [Video]() - if let relatedStreams = attributes["relatedStreams"] { - videos = extractVideos(from: relatedStreams) - } - - let name = attributes["name"]?.string ?? - attributes["uploaderName"]?.string ?? - attributes["uploader"]?.string ?? "" - - let thumbnailURL = attributes["avatarUrl"]?.url ?? - attributes["uploaderAvatar"]?.url ?? - attributes["avatar"]?.url ?? - attributes["thumbnail"]?.url - - return Channel( - id: id, - name: name, - thumbnailURL: thumbnailURL, - subscriptionsCount: subscriptionsCount, - videos: videos - ) - } - - private func extractVideos(from content: JSON) -> [Video] { - content.arrayValue.compactMap(extractVideo(from:)) - } - - private func extractVideo(from content: JSON) -> Video? { - let details = content.dictionaryValue - - if let url = details["url"]?.string { - guard url.contains("/watch") else { - return nil - } - } - - let channelId = details["uploaderUrl"]?.string?.components(separatedBy: "/").last ?? "unknown" - - let thumbnails: [Thumbnail] = Thumbnail.Quality.allCases.compactMap { - if let url = buildThumbnailURL(from: content, quality: $0) { - return Thumbnail(url: url, quality: $0) - } - - return nil - } - - let author = details["uploaderName"]?.string ?? details["uploader"]?.string ?? "" - let authorThumbnailURL = details["avatarUrl"]?.url ?? details["uploaderAvatar"]?.url ?? details["avatar"]?.url - let subscriptionsCount = details["uploaderSubscriberCount"]?.int - - let uploaded = details["uploaded"]?.double - var published = (uploaded.isNil || uploaded == -1) ? nil : (uploaded! / 1000).formattedAsRelativeTime() - if published.isNil { - published = (details["uploadedDate"] ?? details["uploadDate"])?.string ?? "" - } - - let live = details["livestream"]?.bool ?? (details["duration"]?.int == -1) - - let description = extractDescription(from: content) ?? "" - - return Video( - videoID: extractID(from: content), - title: details["title"]?.string ?? "", - author: author, - length: details["duration"]?.double ?? 0, - published: published ?? "", - views: details["views"]?.int ?? 0, - description: description, - channel: Channel(id: channelId, name: author, thumbnailURL: authorThumbnailURL, subscriptionsCount: subscriptionsCount), - thumbnails: thumbnails, - live: live, - likes: details["likes"]?.int, - dislikes: details["dislikes"]?.int, - streams: extractStreams(from: content), - related: extractRelated(from: content) - ) - } - - private func buildThumbnailURL(from content: JSON, quality: Thumbnail.Quality) -> URL? { - guard let thumbnailURL = extractThumbnailURL(from: content) else { - return nil - } - - return URL(string: thumbnailURL - .absoluteString - .replacingOccurrences(of: "hqdefault", with: quality.filename) - .replacingOccurrences(of: "maxresdefault", with: quality.filename) - ) - } - - private func extractID(from content: JSON) -> Video.ID { - content.dictionaryValue["url"]?.string?.components(separatedBy: "?v=").last ?? - extractThumbnailURL(from: content)?.relativeString.components(separatedBy: "/")[5].replacingFirstOccurrence(of: ".png", with: "") ?? "" - } - - private func extractDescription(from content: JSON) -> String? { - guard var description = content.dictionaryValue["description"]?.string else { - return nil - } - - description = description.replacingOccurrences( - of: "
|
|
", - with: "\n", - options: .regularExpression, - range: nil - ) - - let linkRegex = #"(]*?\s+)?href=\"[^"]*\">[^<]*<\/a>)"# - let hrefRegex = #"href=\"([^"]*)\">"# - guard let hrefRegex = try? NSRegularExpression(pattern: hrefRegex) else { return description } - - description = description.replacingMatches(regex: linkRegex) { matchingGroup in - let results = hrefRegex.matches(in: matchingGroup, range: NSRange(matchingGroup.startIndex..., in: matchingGroup)) - - if let result = results.first { - if let swiftRange = Range(result.range(at: 1), in: matchingGroup) { - return String(matchingGroup[swiftRange]) - } - } - - return matchingGroup - } - - description = description.replacingOccurrences(of: "&", with: "&") - - description = description.replacingOccurrences( - of: "<[^>]+>", - with: "", - options: .regularExpression, - range: nil - ) - - return description - } - - private func extractStreams(from content: JSON) -> [Stream] { - var streams = [Stream]() - - if let hlsURL = content.dictionaryValue["hls"]?.url { - streams.append(Stream(instance: account.instance, hlsURL: hlsURL)) - } - - let audioStreams = content - .dictionaryValue["audioStreams"]? - .arrayValue - .filter { $0.dictionaryValue["format"]?.string == "M4A" } - .sorted { - $0.dictionaryValue["bitrate"]?.int ?? 0 > - $1.dictionaryValue["bitrate"]?.int ?? 0 - } ?? [] - - guard let audioStream = audioStreams.first else { - return streams - } - - let videoStreams = content.dictionaryValue["videoStreams"]?.arrayValue ?? [] - - videoStreams.forEach { videoStream in - let videoCodec = videoStream.dictionaryValue["codec"]?.string ?? "" - - guard let audioAssetUrl = audioStream.dictionaryValue["url"]?.url, - let videoAssetUrl = videoStream.dictionaryValue["url"]?.url - else { - return - } - - let audioAsset = AVURLAsset(url: audioAssetUrl) - let videoAsset = AVURLAsset(url: videoAssetUrl) - - let videoOnly = videoStream.dictionaryValue["videoOnly"]?.bool ?? true - let quality = videoStream.dictionaryValue["quality"]?.string ?? "unknown" - let qualityComponents = quality.components(separatedBy: "p") - let fps = qualityComponents.count > 1 ? Int(qualityComponents[1]) : 30 - let resolution = Stream.Resolution.from(resolution: quality, fps: fps) - let videoFormat = videoStream.dictionaryValue["format"]?.string - - if videoOnly { - streams.append( - Stream( - instance: account.instance, - audioAsset: audioAsset, - videoAsset: videoAsset, - resolution: resolution, - kind: .adaptive, - videoFormat: videoFormat - ) - ) - } else { - streams.append( - SingleAssetStream( - instance: account.instance, - avAsset: videoAsset, - resolution: resolution, - kind: .stream - ) - ) - } - } - - return streams - } - - private func extractRelated(from content: JSON) -> [Video] { - content - .dictionaryValue["relatedStreams"]? - .arrayValue - .compactMap(extractVideo(from:)) ?? [] - } - - private func extractThumbnailURL(from content: JSON) -> URL? { - content.dictionaryValue["thumbnail"]?.url ?? content.dictionaryValue["thumbnailUrl"]?.url - } - - private func extractContentItem(from content: JSON) -> ContentItem? { - let details = content.dictionaryValue - - let contentType: ContentItem.ContentType - - if let url = details["url"]?.string { - if url.contains("/playlist") { - contentType = .playlist - } else if url.contains("/channel") { - contentType = .channel - } else { - contentType = .video - } - } else { - contentType = .video - } - - switch contentType { - case .video: - if let video = extractVideo(from: content) { - return ContentItem(video: video) - } - default: - return nil - } - - return nil - } - - private func extractContentItems(from content: JSON) -> [ContentItem] { - content.arrayValue.compactMap { extractContentItem(from: $0) } - } -} diff --git a/Model/Applications/VideosApp.swift b/Model/Applications/VideosApp.swift index 080490fd..35cd9cdb 100644 --- a/Model/Applications/VideosApp.swift +++ b/Model/Applications/VideosApp.swift @@ -2,7 +2,6 @@ import Foundation enum VideosApp: String, CaseIterable { case invidious, piped - case demoApp var name: String { rawValue.capitalized @@ -65,6 +64,6 @@ enum VideosApp: String, CaseIterable { } var supportsOpeningVideosByID: Bool { - self != .demoApp + true } } diff --git a/Shared/Defaults.swift b/Shared/Defaults.swift index 179a0a7f..eea54a22 100644 --- a/Shared/Defaults.swift +++ b/Shared/Defaults.swift @@ -9,7 +9,7 @@ extension Defaults.Keys { static let instancesManifest = Key("instancesManifest", default: "") static let countryOfPublicInstances = Key("countryOfPublicInstances") - static let instances = Key<[Instance]>("instances", default: [.init(app: .demoApp, name: "Demo", apiURL: "")]) + static let instances = Key<[Instance]>("instances", default: []) static let accounts = Key<[Account]>("accounts", default: []) static let lastAccountID = Key("lastAccountID") static let lastInstanceID = Key("lastInstanceID") diff --git a/Shared/Settings/AccountsNavigationLink.swift b/Shared/Settings/AccountsNavigationLink.swift index bc3a68ad..9290f284 100644 --- a/Shared/Settings/AccountsNavigationLink.swift +++ b/Shared/Settings/AccountsNavigationLink.swift @@ -8,7 +8,6 @@ struct AccountsNavigationLink: View { NavigationLink(instance.longDescription) { InstanceSettings(instance: instance) } - .disabled(instance.app == .demoApp) .buttonStyle(.plain) .contextMenu { removeInstanceButton(instance) diff --git a/Yattee.xcodeproj/project.pbxproj b/Yattee.xcodeproj/project.pbxproj index 33a5b996..a32268c0 100644 --- a/Yattee.xcodeproj/project.pbxproj +++ b/Yattee.xcodeproj/project.pbxproj @@ -440,9 +440,6 @@ 376CD21726FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376CD21526FBE18D001E1AC1 /* Instance+Fixtures.swift */; }; 376CD21826FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376CD21526FBE18D001E1AC1 /* Instance+Fixtures.swift */; }; 376E331228AD3B320070E30C /* ScrollDismissesKeyboard+Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376E331128AD3B320070E30C /* ScrollDismissesKeyboard+Backport.swift */; }; - 3771429829087DFC00306CEA /* DemoAppAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3771429729087DFC00306CEA /* DemoAppAPI.swift */; }; - 3771429929087DFC00306CEA /* DemoAppAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3771429729087DFC00306CEA /* DemoAppAPI.swift */; }; - 3771429A29087DFC00306CEA /* DemoAppAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3771429729087DFC00306CEA /* DemoAppAPI.swift */; }; 3772003827E8EEB100CB2475 /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3772003227E8EEA100CB2475 /* AudioToolbox.framework */; }; 3772003927E8EEB700CB2475 /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3772003427E8EEA100CB2475 /* AVFoundation.framework */; }; 3772003A27E8EEBE00CB2475 /* CoreMedia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3772003527E8EEA100CB2475 /* CoreMedia.framework */; }; @@ -1178,7 +1175,6 @@ 376E331128AD3B320070E30C /* ScrollDismissesKeyboard+Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ScrollDismissesKeyboard+Backport.swift"; sourceTree = ""; }; 3771429529087BE100306CEA /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; }; 3771429629087BF000306CEA /* az */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = az; path = az.lproj/Localizable.strings; sourceTree = ""; }; - 3771429729087DFC00306CEA /* DemoAppAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoAppAPI.swift; sourceTree = ""; }; 3772002527E8ED2600CB2475 /* BridgingHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BridgingHeader.h; sourceTree = ""; }; 3772003127E8EEA100CB2475 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS15.4.sdk/usr/lib/libz.tbd; sourceTree = DEVELOPER_DIR; }; 3772003227E8EEA100CB2475 /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS15.4.sdk/System/Library/Frameworks/AudioToolbox.framework; sourceTree = DEVELOPER_DIR; }; @@ -1775,7 +1771,6 @@ 3700155A271B0D4D0049C794 /* PipedAPI.swift */, 37D526DD2720AC4400ED2F5E /* VideosAPI.swift */, 376A33DF2720CAD6000C1D6B /* VideosApp.swift */, - 3771429729087DFC00306CEA /* DemoAppAPI.swift */, ); path = Applications; sourceTree = ""; @@ -2934,7 +2929,6 @@ 3711403F26B206A6005B3555 /* SearchModel.swift in Sources */, 3729037E2739E47400EA99F6 /* MenuCommands.swift in Sources */, 37F64FE426FE70A60081B69E /* RedrawOnModifier.swift in Sources */, - 3771429829087DFC00306CEA /* DemoAppAPI.swift in Sources */, 37EBD8C427AF0DA800F1C24B /* PlayerBackend.swift in Sources */, 376A33E02720CAD6000C1D6B /* VideosApp.swift in Sources */, 374AB3DB28BCAF7E00DF56FB /* SeekType.swift in Sources */, @@ -3334,7 +3328,6 @@ 373CFAF02697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */, 3763495226DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */, 370015AA28BBAE7F000149FD /* ProgressBar.swift in Sources */, - 3771429929087DFC00306CEA /* DemoAppAPI.swift in Sources */, 371B7E672759786B00D21217 /* Comment+Fixtures.swift in Sources */, 3769C02F2779F18600DDB3EA /* PlaceholderProgressView.swift in Sources */, 37BA794426DBA973002A0235 /* PlaylistsModel.swift in Sources */, @@ -3482,7 +3475,6 @@ 3752069B285E8DD300CA655F /* Chapter.swift in Sources */, 37B044B926F7AB9000E1419D /* SettingsView.swift in Sources */, 3743B86A27216D3600261544 /* ChannelCell.swift in Sources */, - 3771429A29087DFC00306CEA /* DemoAppAPI.swift in Sources */, 37E80F47287B7B9400561799 /* VideoDetailsOverlay.swift in Sources */, 37E80F44287B7AB400561799 /* VideoDetails.swift in Sources */, 3751BA8527E6914F007B1A60 /* ReturnYouTubeDislikeAPI.swift in Sources */,