Skip to content

Commit

Permalink
Merge pull request #31 from weiran/folders
Browse files Browse the repository at this point in the history
Added support for all other folders
  • Loading branch information
weiran authored May 8, 2020
2 parents e1f483a + c20c152 commit e60d44b
Show file tree
Hide file tree
Showing 16 changed files with 200 additions and 177 deletions.
4 changes: 2 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
language: objective-c
osx_image: xcode11.3
osx_image: xcode11.4
xcode_workspace: WatchItLater.xcworkspace
xcode_scheme: WatchItLater
before_install:
Expand All @@ -10,4 +10,4 @@ install:
before_script:
- mv 'App/Supporting Files/InstapaperConfiguration.defaults.plist' 'App/Supporting Files/InstapaperConfiguration.plist'
script:
- travis_wait 30 set -o pipefail && xcodebuild -workspace ${TRAVIS_XCODE_WORKSPACE} -scheme ${TRAVIS_XCODE_SCHEME} -sdk appletvsimulator -configuration Debug | xcpretty
- xcodebuild -workspace ${TRAVIS_XCODE_WORKSPACE} -scheme ${TRAVIS_XCODE_SCHEME} -sdk appletvsimulator -configuration Debug | xcpretty
2 changes: 1 addition & 1 deletion App/Base.lproj/Main.storyboard
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="kw5-6F-i0c">
<rect key="frame" x="0.0" y="0.0" width="960" height="1000"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="qEv-cj-EVp" customClass="AsyncImageView">
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="qEv-cj-EVp">
<rect key="frame" x="64" y="64" width="832" height="468"/>
<constraints>
<constraint firstAttribute="width" secondItem="qEv-cj-EVp" secondAttribute="height" multiplier="16:9" id="aQj-nM-IyX"/>
Expand Down
28 changes: 15 additions & 13 deletions App/DetailViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import UIKit
import AVKit
import TVUIKit

import AsyncImageView
import Kingfisher
import PromiseKit
import TVVLCPlayer
import SwiftyUserDefaults
Expand All @@ -30,14 +30,16 @@ class DetailViewController: UIViewController {
@IBOutlet weak var durationLabel: UILabel!
@IBOutlet weak var qualityLabel: UILabel!
@IBOutlet weak var descriptionLabel: UILabel!
@IBOutlet weak var thumbnailImageView: AsyncImageView!
@IBOutlet weak var thumbnailImageView: UIImageView!
@IBOutlet weak var playButton: UIButton!
@IBOutlet weak var archiveButton: UIButton!
@IBOutlet weak var activityIndicator: UIActivityIndicatorView!
@IBOutlet weak var buttonsStackView: UIStackView!

override func viewDidLoad() {
super.viewDidLoad()

instapaperAPI?.storedAuth().cauterize()

durationLabel.text = " "
qualityLabel.text = " "
Expand All @@ -48,14 +50,10 @@ class DetailViewController: UIViewController {

if let videoProvider = try? VideoProvider.videoProvider(for: video.urlString) {
self.videoProvider = videoProvider
videoProvider.thumbnailURL().done { [weak self] url in
self?.thumbnailImageView.imageURL = url
}.cauterize()
videoProvider.duration().done { [weak self] (duration: Double) -> Void in
self?.durationLabel.text = self?.formatTimeInterval(duration: duration)
self?.duration = CMTime(seconds: duration, preferredTimescale: CMTimeScale(duration * 60))
}.cauterize()
videoProvider.videoStream(preferredFormatType: Defaults[\.defaultVideoQualityKey]).done { [weak self] (videoStream) in
self?.thumbnailImageView.kf.setImage(with: videoStream.thumbnailURL)
self?.durationLabel.text = self?.formatTimeInterval(duration: videoStream.duration)
self?.duration = CMTime(seconds: videoStream.duration, preferredTimescale: CMTimeScale(videoStream.duration * 60))
if let format = videoStream.videoFormatType {
self?.qualityLabel.text = format.description()
}
Expand Down Expand Up @@ -106,7 +104,6 @@ class DetailViewController: UIViewController {
guard let self = self, let video = self.video else {
return
}

self.videoStream = videoStream

if let alertController = self.playFromPositionAlertController(video) {
Expand All @@ -124,10 +121,15 @@ class DetailViewController: UIViewController {

@IBAction func didArchive(_ sender: Any) {
if let video = video, let instapaperAPI = instapaperAPI {
instapaperAPI.archive(id: video.id)
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "VideoArchived"), object: sender, userInfo: ["video": video])
instapaperAPI.archive(id: video.id).done { [weak self] in
self?.dismiss(animated: true, completion: nil)
}.catch { [weak self] error in
let alert = UIAlertController(
title: "Error archiving", message: "There was a problem archiving this video.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .cancel))
self?.present(alert, animated: true)
}
}
dismiss(animated: true, completion: nil)
}

private func showVideoPlayer(startFrom: Int? = nil) {
Expand Down
4 changes: 3 additions & 1 deletion App/FoldersTabBarController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,18 @@ class FoldersTabBarController: UITabBarController {
let unreadViewController = viewController(for: .unread, with: storyboard)
let starredViewController = viewController(for: .starred, with: storyboard)
let archiveViewController = viewController(for: .archive, with: storyboard)
let otherViewController = viewController(for: .other, with: storyboard)
let settingsViewController = storyboard.instantiateViewController(identifier: "SettingsViewController")

self.viewControllers = [unreadViewController, starredViewController, archiveViewController, settingsViewController]
self.viewControllers = [unreadViewController, starredViewController, archiveViewController, otherViewController, settingsViewController]

guard let tabBarItems = self.tabBar.items else { return }
for (index, element) in tabBarItems.enumerated() {
switch index {
case 0: element.title = "New"
case 1: element.title = "Starred"
case 2: element.title = "Archive"
case 3: element.title = "Other"
default: break
}
}
Expand Down
86 changes: 74 additions & 12 deletions App/Providers/InstapaperAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,27 @@ protocol API {

func login(username: String, password: String) -> Promise<Void>
func storedAuth() -> Promise<Void>
func fetch(_ folder: InstapaperFolder) -> Promise<[Video]>
func fetch(_ folders: [Int]) -> Promise<[Video]>
}

enum InstapaperFolder: Int {
case unread = -1
case starred = -2
case archive = -3
case other = -999
}

class InstapaperAPI: NSObject, API, IKEngineDelegate {
static var name = "Instapaper"
private var engine: IKEngine

private var loginSeal: Resolver<Void>?
private var fetchFoldersSeal: Resolver<[Int]>?
private var fetchSeal: Resolver<[Video]>?
private var archiveSeal: Resolver<Void>?

private var foldersToFetch: [IKFolder]?
private var fetchedVideos: [IKBookmark]?

override init() {
let (consumerKey, consumerSecret) = InstapaperAPI.getOAuthConfiguration()
Expand Down Expand Up @@ -66,7 +72,8 @@ class InstapaperAPI: NSObject, API, IKEngineDelegate {
return engine.oAuthToken != nil && engine.oAuthTokenSecret != nil
}
}


// this must be called first to set auth tokens
@discardableResult
func storedAuth() -> Promise<Void> {
let (promise, seal) = Promise<Void>.pending()
Expand All @@ -83,19 +90,34 @@ class InstapaperAPI: NSObject, API, IKEngineDelegate {
return promise
}

func fetch(_ folder: InstapaperFolder = .unread) -> Promise<[Video]> {
func fetch(_ folders: [Int]) -> Promise<[Video]> {
let (promise, seal) = Promise<[Video]>.pending()

self.fetchSeal = seal
let instapaperFolder = IKFolder(folderID: folder.rawValue)
engine.bookmarks(in: instapaperFolder, limit: 500, existingBookmarks: nil, userInfo: nil)
self.foldersToFetch = []
self.fetchedVideos = []

folders.forEach { folder in
let instapaperFolder = IKFolder(folderID: folder)!
engine.bookmarks(in: instapaperFolder, limit: 500, existingBookmarks: nil, userInfo: nil)
self.foldersToFetch?.append(instapaperFolder)
}

return promise
}

func fetchFolders() -> Promise<[Int]> {
let (promise, seal) = Promise<[Int]>.pending()
self.fetchFoldersSeal = seal
engine.folders(withUserInfo: nil)
return promise
}

func archive(id: Int) {
func archive(id: Int) -> Promise<Void> {
let (promise, seal) = Promise<Void>.pending()
self.archiveSeal = seal
let bookmark = IKBookmark(bookmarkID: id)
engine.archiveBookmark(bookmark, userInfo: nil)
return promise
}

func engine(_ engine: IKEngine!, connection: IKURLConnection!, didReceiveAuthToken token: String!, andTokenSecret secret: String!) {
Expand All @@ -114,12 +136,44 @@ class InstapaperAPI: NSObject, API, IKEngineDelegate {

func engine(_ engine: IKEngine!, connection: IKURLConnection!, didReceiveBookmarks bookmarks: [Any]!, of user: IKUser!, for folder: IKFolder!) {
if let bookmarks = bookmarks as! [IKBookmark]? {
let videos = bookmarks.map { (bookmark) -> Video in
return Video(bookmark)
guard var fetchedVideos = self.fetchedVideos,
var foldersToFetch = self.foldersToFetch else {
self.fetchSeal?.reject(VideoError.UnknownError)
return
}
fetchSeal?.fulfill(videos)
fetchSeal = nil

fetchedVideos.append(contentsOf: bookmarks)
foldersToFetch = foldersToFetch.filter { $0.folderID != folder.folderID }

if foldersToFetch.isEmpty {
var videos = fetchedVideos.map { (bookmark) -> Video in
return Video(bookmark)
}
videos = videos.sorted { $0.date > $1.date }
self.fetchSeal?.fulfill(videos)

self.fetchSeal = nil
self.fetchedVideos = nil
self.foldersToFetch = nil
} else {
self.fetchedVideos = fetchedVideos
self.foldersToFetch = foldersToFetch
}
}
}

func engine(_ engine: IKEngine!, connection: IKURLConnection!, didReceiveFolders folders: [Any]!) {
guard let folders = folders as! [IKFolder]? else { return }
let folderIDs = folders.map { folder in
return folder.folderID
}
fetchFoldersSeal?.fulfill(folderIDs)
fetchFoldersSeal = nil
}

func engine(_ engine: IKEngine!, connection: IKURLConnection!, didArchiveBookmark bookmark: IKBookmark!) {
archiveSeal?.fulfill(())
archiveSeal = nil
}

func engine(_ engine: IKEngine!, didFail connection: IKURLConnection!, error: Error!) {
Expand All @@ -131,7 +185,15 @@ class InstapaperAPI: NSObject, API, IKEngineDelegate {
case .bookmarksList:
fetchSeal?.reject(error)
fetchSeal = nil


case .foldersList:
fetchFoldersSeal?.reject(error)
fetchFoldersSeal = nil

case .bookmarksArchive:
archiveSeal?.reject(error)
archiveSeal = nil

default:
return
}
Expand Down
1 change: 1 addition & 0 deletions App/Providers/VideoError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ enum VideoError: Error {
case InvalidURL
case NoStreamURLFound
case NoThumbnailURLFound
case UnknownError
}
8 changes: 6 additions & 2 deletions App/Providers/VideoProviderProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ protocol VideoProviderProtocol {
init(_ url: String) throws

func videoStream(preferredFormatType: VideoFormatType?) -> Promise<VideoStream>
func thumbnailURL() -> Promise<URL>
func duration() -> Promise<Double>
}

extension VideoProviderProtocol {
func videoStream(preferredFormatType: VideoFormatType? = nil) -> Promise<VideoStream> {
return videoStream(preferredFormatType: preferredFormatType)
}
}
12 changes: 11 additions & 1 deletion App/Providers/VideoStream.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,21 @@
struct VideoStream {
let videoURL: URL!
let audioURL: URL?
let duration: Double
let videoFormatType: VideoFormatType?
let thumbnailURL: URL?

init(videoURL: URL, audioURL: URL?, videoFormatType: VideoFormatType? = nil) {
init(
videoURL: URL,
audioURL: URL?,
duration: Double,
videoFormatType: VideoFormatType? = nil,
thumbnailURL: URL?
) {
self.videoURL = videoURL
self.audioURL = audioURL
self.duration = duration
self.videoFormatType = videoFormatType
self.thumbnailURL = thumbnailURL
}
}
53 changes: 15 additions & 38 deletions App/Providers/VimeoProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,28 @@ class VimeoProvider: VideoProviderProtocol {
self.url = URL(string: url)!
}

func videoStream(preferredFormatType: VideoFormatType?) -> Promise<VideoStream> {
func videoStream(preferredFormatType: VideoFormatType? = nil) -> Promise<VideoStream> {
let (promise, seal) = Promise<VideoStream>.pending()

YTVimeoExtractor.shared().fetchVideo(withVimeoURL: url.absoluteString, withReferer: nil) { video, error in
if let streamURL = video?.highestQualityStreamURL() {
if let video = video {
/// TODO we're cheating here by guessing the top quality video is 1080p
/// and going for the top quality no matter what the setting is
/// we need to map between selected max format and Vimeo format types to
/// select the right one
seal.fulfill(VideoStream(videoURL: streamURL, audioURL: nil, videoFormatType: .video1080p))
let streamURL = video.highestQualityStreamURL()
let duration = Double(video.duration)
let thumbnailURL = video.thumbnailURLs?[NSNumber(value: YTVimeoVideoThumbnailQuality.HD.rawValue)] ??
video.thumbnailURLs?[NSNumber(value: YTVimeoVideoThumbnailQuality.medium.rawValue)] ??
video.thumbnailURLs?[NSNumber(value: YTVimeoVideoThumbnailQuality.small.rawValue)]
let videoStream = VideoStream(
videoURL: streamURL,
audioURL: nil,
duration: duration,
videoFormatType: .video1080p,
thumbnailURL: thumbnailURL
)
seal.fulfill(videoStream)
} else if let error = error {
seal.reject(error)
} else {
Expand All @@ -35,39 +47,4 @@ class VimeoProvider: VideoProviderProtocol {

return promise
}

func thumbnailURL() -> Promise<URL> {
let (promise, seal) = Promise<URL>.pending()

YTVimeoExtractor.shared().fetchVideo(withVimeoURL: url.absoluteString, withReferer: nil) { video, error in
if let thumbnailURLs = video?.thumbnailURLs,
let thumbnailURL = thumbnailURLs[NSNumber(value: YTVimeoVideoThumbnailQuality.HD.rawValue)] ??
thumbnailURLs[NSNumber(value: YTVimeoVideoThumbnailQuality.medium.rawValue)] ??
thumbnailURLs[NSNumber(value: YTVimeoVideoThumbnailQuality.small.rawValue)] {
seal.fulfill(thumbnailURL)
} else if let error = error {
seal.reject(error)
} else {
seal.reject(VideoError.NoThumbnailURLFound)
}
}

return promise
}

func duration() -> Promise<Double> {
let (promise, seal) = Promise<Double>.pending()

YTVimeoExtractor.shared().fetchVideo(withVimeoURL: url.absoluteString, withReferer: nil) { video, error in
if let video = video {
seal.fulfill(video.duration)
} else if let error = error {
seal.reject(error)
} else {
seal.reject(VideoError.InvalidURL)
}
}

return promise
}
}
Loading

0 comments on commit e60d44b

Please sign in to comment.