├── .gitignore ├── Podfile ├── Podfile.lock ├── PopcornTime.xcodeproj ├── project.pbxproj └── xcshareddata │ └── xcschemes │ └── PopcornTime.xcscheme ├── PopcornTime ├── Controllers │ ├── AnimeDetailsViewController.swift │ ├── AnimeViewController.swift │ ├── BarHidingViewController.swift │ ├── BaseCollectionViewController.swift │ ├── BaseDetailsViewController.swift │ ├── ColorfullTabBarController.swift │ ├── FavoritesViewController.swift │ ├── LoadingViewController.swift │ ├── MovieDetailsViewController.swift │ ├── MoviesViewController.swift │ ├── OAuthViewController.swift │ ├── PagedViewController.swift │ ├── ParseViewController.swift │ ├── SettingsViewController.swift │ ├── ShowDetailsViewController.swift │ └── ShowsViewController.swift ├── Info.plist ├── Models │ ├── APIManager.swift │ ├── Anime.swift │ ├── AppDelegate.swift │ ├── BaseStructures.swift │ ├── BasicInfo.swift │ ├── DataManager.swift │ ├── Extensions.swift │ ├── Image.swift │ ├── ImageProvider.swift │ ├── Movie.swift │ ├── PTAPIManager.h │ ├── PTAPIManager.m │ ├── PTTorrentStreamer.h │ ├── PTTorrentStreamer.mm │ ├── ParseManager.swift │ └── Show.swift ├── PopcornTime-Bridging-Header.h ├── Resources │ ├── Images.xcassets │ │ ├── AnimeIcon.imageset │ │ │ ├── Contents.json │ │ │ └── anime.png │ │ ├── AppIcon.appiconset │ │ │ ├── AppIcon60@2x.png │ │ │ ├── AppIcon76.png │ │ │ ├── AppIcon76@2x.png │ │ │ └── Contents.json │ │ ├── BigLogo.imageset │ │ │ ├── Contents.json │ │ │ └── popcorn-time-logo.png │ │ ├── Contents.json │ │ └── SubwayIconSet │ │ │ ├── AddToFavoritesIcon.imageset │ │ │ ├── Contents.json │ │ │ └── icon_087@2x.png │ │ │ ├── FavoritesIcon.imageset │ │ │ ├── Contents.json │ │ │ └── icon_086@2x.png │ │ │ ├── MoviesIcon.imageset │ │ │ ├── Contents.json │ │ │ └── icon_0281@2x.png │ │ │ ├── RemoveFromFavoritesIcon.imageset │ │ │ ├── Contents.json │ │ │ └── icon_088@2x.png │ │ │ ├── SettingsIcon.imageset │ │ │ ├── Contents.json │ │ │ └── icon_0186@2x.png │ │ │ └── ShowsIcon.imageset │ │ │ ├── Contents.json │ │ │ └── icon_0304@2x.png │ ├── Launch Screen.xib │ ├── Main.storyboard │ └── OpenSans-Regular.ttf └── Views │ ├── EpisodeCell.swift │ ├── EpisodeCell.xib │ ├── MoreShowsCollectionViewCell.swift │ ├── MoreShowsCollectionViewCell.xib │ ├── SeasonHeader.swift │ ├── SeasonHeader.xib │ ├── ShowCollectionViewCell.swift │ ├── ShowCollectionViewCell.xib │ ├── StratchyHeader.swift │ ├── StratchyHeader.xib │ └── StratchyHeaderLayout.swift ├── README.md ├── Screenshots ├── 1.png ├── 2.png └── 3.png ├── Thirdparties └── VLCKit │ └── Dropin-Player │ ├── VDLPlaybackViewController.h │ ├── VDLPlaybackViewController.m │ └── VDLPlaybackViewController.xib └── popcorntime_api.paw /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io 2 | 3 | ### OSX ### 4 | .DS_Store 5 | .AppleDouble 6 | .LSOverride 7 | 8 | # Icon must end with two \r 9 | Icon 10 | 11 | 12 | # Thumbnails 13 | ._* 14 | 15 | # Files that might appear on external disk 16 | .Spotlight-V100 17 | .Trashes 18 | 19 | # Directories potentially created on remote AFP share 20 | .AppleDB 21 | .AppleDesktop 22 | Network Trash Folder 23 | Temporary Items 24 | .apdisk 25 | 26 | 27 | ### Xcode ### 28 | build/ 29 | *.pbxuser 30 | !default.pbxuser 31 | *.mode1v3 32 | !default.mode1v3 33 | *.mode2v3 34 | !default.mode2v3 35 | *.perspectivev3 36 | !default.perspectivev3 37 | xcuserdata 38 | *.xccheckout 39 | *.moved-aside 40 | DerivedData 41 | *.xcuserstate 42 | 43 | 44 | ### Objective-C ### 45 | # Xcode 46 | # 47 | build/ 48 | *.pbxuser 49 | !default.pbxuser 50 | *.mode1v3 51 | !default.mode1v3 52 | *.mode2v3 53 | !default.mode2v3 54 | *.perspectivev3 55 | !default.perspectivev3 56 | xcuserdata 57 | *.xccheckout 58 | *.moved-aside 59 | DerivedData 60 | *.hmap 61 | *.ipa 62 | *.xcuserstate 63 | 64 | # CocoaPods 65 | # 66 | # We recommend against adding the Pods directory to your .gitignore. However 67 | # you should judge for yourself, the pros and cons are mentioned at: 68 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 69 | # 70 | *.xcworkspace/ 71 | Pods/ -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '8.0' 3 | inhibit_all_warnings! 4 | 5 | source 'https://github.com/danylokostyshyn/private-podspecs.git' 6 | source 'https://github.com/CocoaPods/Specs' 7 | 8 | target 'PopcornTime' do 9 | 10 | pod 'private-MobileVLCKit' 11 | pod 'private-boost' 12 | pod 'private-libtorrent' 13 | pod 'private-openssl' 14 | pod 'Reachability' 15 | pod 'CocoaSecurity' 16 | pod 'SDWebImage' 17 | 18 | pod 'Crashlytics' 19 | pod 'Fabric' 20 | 21 | end 22 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - CocoaSecurity (1.2.4) 3 | - Crashlytics (3.7.0): 4 | - Fabric (~> 1.6.3) 5 | - Fabric (1.6.6) 6 | - private-boost (1.58.0) 7 | - private-libtorrent (1.0.6) 8 | - private-MobileVLCKit (2.5.150604) 9 | - private-openssl (1.0.204) 10 | - Reachability (3.2) 11 | - SDWebImage (3.7.5): 12 | - SDWebImage/Core (= 3.7.5) 13 | - SDWebImage/Core (3.7.5) 14 | 15 | DEPENDENCIES: 16 | - CocoaSecurity 17 | - Crashlytics 18 | - Fabric 19 | - private-boost 20 | - private-libtorrent 21 | - private-MobileVLCKit 22 | - private-openssl 23 | - Reachability 24 | - SDWebImage 25 | 26 | SPEC CHECKSUMS: 27 | CocoaSecurity: d288a6f87e0f363823d2cb83e753814a6944f71a 28 | Crashlytics: c3a2333dea9e2733d2777f730910321fc9e25c0d 29 | Fabric: 1abc1e59f11fa39ede6f690ef7309d8563e3ec59 30 | private-boost: 390bdadecb82f24bcec47068f4b041acc757d575 31 | private-libtorrent: 9f0d4c5e1b3842e3baf5493b66cadb684bb07afd 32 | private-MobileVLCKit: 9f36464901370c0dd54e2fa2fe34b44effc18cb3 33 | private-openssl: b469f3f9eca633bcc9e888c1f1d4fc27b7ea7c17 34 | Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96 35 | SDWebImage: 69c6303e3348fba97e03f65d65d4fbc26740f461 36 | 37 | COCOAPODS: 0.39.0 38 | -------------------------------------------------------------------------------- /PopcornTime.xcodeproj/xcshareddata/xcschemes/PopcornTime.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /PopcornTime/Controllers/AnimeDetailsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShowDetailsViewController.swift 3 | // PopcornTime 4 | // 5 | // Created by Andrew K. on 3/13/15. 6 | // Copyright (c) 2015 PopcornTime. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class AnimeDetailsViewController: BaseDetailsViewController { 12 | 13 | var anime: Anime! { 14 | get { 15 | return self.item as! Anime 16 | } 17 | } 18 | // MARK: - UIViewController 19 | 20 | override func viewDidLoad() { 21 | super.viewDidLoad() 22 | preferedOtherHeadersHeight = 0.0 23 | } 24 | 25 | // MARK: - BaseDetailsViewController 26 | 27 | override func reloadData() { 28 | PTAPIManager.shared().showInfo(with: .anime, withId: item.identifier, success: { (item) -> Void in 29 | guard let item = item else { return } 30 | self.anime.update(item) 31 | self.collectionView?.reloadData() 32 | }, failure: nil) 33 | } 34 | 35 | // MARK: - DetailViewControllerDataSource 36 | override func numberOfSeasons() -> Int { 37 | return anime.seasons.count 38 | } 39 | 40 | override func numberOfEpisodesInSeason(_ seasonsIndex: Int) -> Int { 41 | return anime.seasons[seasonsIndex].episodes.count 42 | } 43 | 44 | override func setupCell(_ cell: EpisodeCell, seasonIndex: Int, episodeIndex: Int) { 45 | let episode = anime.seasons[seasonIndex].episodes[episodeIndex] 46 | cell.titleLabel.text = "\(episode.episodeNumber)" 47 | } 48 | 49 | override func setupSeasonHeader(_ header: SeasonHeader, seasonIndex: Int) { 50 | 51 | } 52 | 53 | override func cellWasPressed(_ cell: UICollectionViewCell, seasonIndex: Int, episodeIndex: Int) { 54 | let episode = anime.seasons[seasonIndex].episodes[episodeIndex] 55 | showVideoPickerPopupForEpisode(episode, basicInfo: self.item, fromView: cell) 56 | } 57 | 58 | override func cellWasLongPressed(_ cell: UICollectionViewCell, seasonIndex: Int, episodeIndex: Int) { 59 | // let episode = anime.episodeFor(seasonIndex: seasonIndex, episodeIndex: episodeIndex) 60 | // let seasonEpisodes = anime.episodesFor(seasonIndex: seasonIndex) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /PopcornTime/Controllers/AnimeViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MoviesViewController.swift 3 | // PopcornTime 4 | // 5 | // Created by Andrew K. on 3/15/15. 6 | // Copyright (c) 2015 PopcornTime. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class AnimeViewController: PagedViewController { 12 | 13 | override var showType: PTItemType { 14 | get { 15 | return .anime 16 | } 17 | } 18 | 19 | override func map(_ response: [AnyObject]) -> [BasicInfo] { 20 | return response.map({ Anime(dictionary: $0 as! [AnyHashable: Any]) }) 21 | } 22 | 23 | func collectionView(_ collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: IndexPath) { 24 | 25 | if let cell = collectionView.cellForItem(at: indexPath){ 26 | //Check if cell is MoreShowsCell 27 | if let _ = cell as? MoreShowsCollectionViewCell{ 28 | loadMore() 29 | } else { 30 | performSegue(withIdentifier: "showDetails", sender: cell) 31 | } 32 | } 33 | } 34 | 35 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 36 | super.prepare(for: segue, sender: sender) 37 | 38 | if segue.identifier == "showDetails"{ 39 | if let episodesVC = segue.destination as? AnimeDetailsViewController{ 40 | if let senderCell = sender as? UICollectionViewCell{ 41 | if let indexPath = collectionView!.indexPath(for: senderCell){ 42 | var item: BasicInfo! 43 | if (searchController!.isActive) { 44 | item = searchResults[indexPath.row] 45 | } else { 46 | item = items[indexPath.row] 47 | } 48 | episodesVC.item = item as! Anime 49 | } 50 | } 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /PopcornTime/Controllers/BarHidingViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BarHidingViewController.swift 3 | // PopcornTime 4 | // 5 | // Created by Andrew K. on 3/13/15. 6 | // Copyright (c) 2015 PopcornTime. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class BarHidingViewController: UIViewController { 12 | 13 | fileprivate var barsVisible:Bool = true { 14 | willSet { 15 | self.tabBarController?.tabBar.isHidden = !newValue 16 | self.navigationController?.navigationBar.isHidden = !newValue 17 | UIApplication.shared.setStatusBarHidden(!newValue, with: .fade) 18 | } 19 | } 20 | 21 | override func viewWillLayoutSubviews() { 22 | super.viewWillLayoutSubviews() 23 | //Hide bars if needed 24 | let sizeClass = (horizontal: self.view.traitCollection.horizontalSizeClass, vertical: self.view.traitCollection.verticalSizeClass) 25 | switch sizeClass{ 26 | case (_,.compact): 27 | self.barsVisible = false 28 | default: self.barsVisible = true 29 | } 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /PopcornTime/Controllers/BaseCollectionViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShowsCollectionViewController.swift 3 | // PopcornTime 4 | // 5 | // Created by Andrew K. on 3/8/15. 6 | // Copyright (c) 2015 PopcornTime. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | let reuseIdentifierShow = "ShowCell" 12 | let reuseIdentifierMore = "MoreShowsCell" 13 | 14 | ///Base class for displaying collection of shows, subclass MUST override reloadData() and set self.shows in it 15 | class BaseCollectionViewController: BarHidingViewController, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource, UICollectionViewDelegate { 16 | 17 | fileprivate struct Constants{ 18 | static let desirediPadCellWidth = 160 19 | static let desirediPadCellHeight = 205 20 | static let numberOfLinesiPhonePortrait = 2 21 | static let numberOfItemsiPhonePortrait = 2 22 | static let numberOfLinesiPhoneLandscape = 2 23 | static let numberOfItemsiPhoneLandscape = 5 24 | } 25 | 26 | var items = [BasicInfo]() 27 | var showLoadMoreCell = false 28 | 29 | 30 | @IBOutlet weak var collectionView: UICollectionView!{ 31 | didSet{ 32 | collectionView.alwaysBounceVertical = true 33 | collectionView.dataSource = self 34 | collectionView.delegate = self 35 | } 36 | } 37 | @IBOutlet weak var collectionViewLayout: UICollectionViewFlowLayout! 38 | 39 | // MARK: UIViewController 40 | override func viewDidLoad() { 41 | super.viewDidLoad() 42 | 43 | self.collectionView!.register(UINib(nibName: "ShowCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: reuseIdentifierShow) 44 | self.collectionView!.register(UINib(nibName: "MoreShowsCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: reuseIdentifierMore) 45 | self.collectionView?.delegate = self 46 | self.collectionView?.collectionViewLayout.invalidateLayout() 47 | 48 | self.reloadData() 49 | } 50 | 51 | // MARK: UICollectionViewDataSource 52 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 53 | let additionalCellsCount = self.showLoadMoreCell ? 1 : 0 54 | return (items.count + additionalCellsCount) 55 | } 56 | 57 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 58 | 59 | if (self.showLoadMoreCell && indexPath.row == items.count) { 60 | //Last cell 61 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifierMore, for: indexPath) as! MoreShowsCollectionViewCell 62 | return cell 63 | } else { 64 | //Ordinary show cell 65 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifierShow, for: indexPath) as! ShowCollectionViewCell 66 | 67 | let item = items[indexPath.row] 68 | cell.title = item.title 69 | 70 | let imageItem = item.smallImage 71 | switch imageItem?.status { 72 | case .new?: 73 | imageItem?.status = .downloading 74 | ImageProvider.sharedInstance.imageFromURL(URL: imageItem?.URL) { (downloadedImage) -> () in 75 | imageItem?.image = downloadedImage 76 | imageItem?.status = .finished 77 | 78 | collectionView.reloadItems(at: [indexPath]) 79 | } 80 | case .finished?: 81 | cell.image = imageItem?.image 82 | default: break 83 | } 84 | 85 | return cell 86 | } 87 | } 88 | 89 | // MARK: UICollectionViewDelegateFlowLayout & UICollectionViewDelegate 90 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 91 | 92 | let visibleAreaHeight = collectionView.bounds.height - navigationController!.navigationBar.bounds.height - UIApplication.shared.statusBarFrame.height - self.tabBarController!.tabBar.bounds.height 93 | let visibleAreaWidth = collectionView.bounds.width 94 | 95 | //Set cell size based on size class. 96 | let sizeClass = (horizontal: self.view.traitCollection.horizontalSizeClass, vertical: self.view.traitCollection.verticalSizeClass) 97 | 98 | if let flowLayout = collectionViewLayout as? UICollectionViewFlowLayout{ 99 | switch sizeClass{ 100 | case (.compact,.regular): 101 | //iPhone portrait 102 | let cellWidth = ((visibleAreaWidth - CGFloat(Constants.numberOfItemsiPhonePortrait - 1)*flowLayout.minimumInteritemSpacing - flowLayout.sectionInset.top - flowLayout.sectionInset.bottom)/CGFloat(Constants.numberOfItemsiPhonePortrait)) 103 | let cellHeight = ((visibleAreaHeight - CGFloat(Constants.numberOfLinesiPhonePortrait - 1)*flowLayout.minimumLineSpacing - flowLayout.sectionInset.left - flowLayout.sectionInset.right)/CGFloat(Constants.numberOfLinesiPhonePortrait)) 104 | return CGSize(width: cellWidth, height: cellHeight) 105 | case (_,.compact): 106 | //iPhone landscape 107 | let cellWidth = ((collectionView.bounds.width - CGFloat(Constants.numberOfItemsiPhoneLandscape - 1)*flowLayout.minimumInteritemSpacing - flowLayout.sectionInset.top - flowLayout.sectionInset.bottom)/CGFloat(Constants.numberOfItemsiPhoneLandscape)) 108 | let cellHeight = ((collectionView.bounds.height - CGFloat(Constants.numberOfLinesiPhoneLandscape - 1)*flowLayout.minimumLineSpacing - flowLayout.sectionInset.left - flowLayout.sectionInset.right)/CGFloat(Constants.numberOfLinesiPhoneLandscape)) 109 | return CGSize(width: cellWidth, height: cellHeight) 110 | case (_,_): 111 | // iPad. Calculate cell size based on desired size 112 | let numberOfLines = Int(visibleAreaHeight) / Constants.desirediPadCellHeight 113 | let betweenLinesSpaceSum = CGFloat(numberOfLines - 1) * flowLayout.minimumLineSpacing 114 | let sectionInsetsVerticalSum = flowLayout.sectionInset.top + flowLayout.sectionInset.bottom 115 | 116 | let adjustedHeight = (visibleAreaHeight - betweenLinesSpaceSum - sectionInsetsVerticalSum)/CGFloat(numberOfLines) 117 | let adjustedWidth = adjustedHeight * CGFloat(Constants.desirediPadCellWidth) / CGFloat(Constants.desirediPadCellHeight) 118 | 119 | return CGSize(width: adjustedWidth, height: adjustedHeight) 120 | } 121 | } 122 | 123 | return CGSize(width: 50, height: 50) 124 | } 125 | 126 | // MARK: - ShowsCollectionViewController 127 | func reloadData(){ 128 | assert(true, "Sublcass MUST override this method") 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /PopcornTime/Controllers/BaseDetailsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseDetailsViewController.swift 3 | // PopcornTime 4 | // 5 | // Created by Danylo Kostyshyn on 3/21/15. 6 | // Copyright (c) 2015 PopcornTime. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// With this protocol we encapsulate calls of collectionView indexPathes. For now we have one extra section at the top (empty one with stratchy header), this way if anything changes here we will change all logic here, and all users of this protocol will not have to hcange anything. So it's a good idea to use seasonIndex, episodeIndex instead of indexPathes. 12 | protocol DetailViewControllerDataSource { 13 | func numberOfSeasons() -> Int 14 | func numberOfEpisodesInSeason(_ seasonsIndex: Int) -> Int 15 | func setupCell(_ cell: EpisodeCell, seasonIndex: Int, episodeIndex: Int) 16 | func setupSeasonHeader(_ header: SeasonHeader, seasonIndex: Int) 17 | func cellWasPressed(_ cell: UICollectionViewCell, seasonIndex: Int, episodeIndex: Int) 18 | func cellWasLongPressed(_ cell: UICollectionViewCell, seasonIndex: Int, episodeIndex: Int) 19 | } 20 | 21 | class BaseDetailsViewController: BarHidingViewController, VDLPlaybackViewControllerDelegate, LoadingViewControllerDelegate, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout, DetailViewControllerDataSource { 22 | 23 | // MARK: - Header related 24 | let headerMinAspectRatio: CGFloat = 0.4 25 | let headerWidthToCollectionWidthKoef: CGFloat = 0.3 26 | var header: StratchyHeader? 27 | 28 | var preferedOtherHeadersHeight: CGFloat = 35 29 | 30 | var headerSize: CGSize { 31 | let width = collectionView.bounds.size.width 32 | let minHeight = width * headerMinAspectRatio 33 | var height = collectionView.bounds.size.height * headerWidthToCollectionWidthKoef 34 | height = max(height, minHeight) 35 | return CGSize(width: width, height: height) 36 | } 37 | 38 | // MARK: - 39 | let cellReuseIdentifier = "EpisodeCell" 40 | let firstHeaderReuseIdentifier = "StratchyHeader" 41 | let otherHeadersReuseIdentifier = "OtherHeader" 42 | let episodeCellReuseIdentifier = "EpisodeCell" 43 | 44 | var layout: StratchyHeaderLayout? 45 | 46 | var item: BasicInfo! { 47 | didSet { 48 | navigationItem.title = item.title 49 | reloadData() 50 | } 51 | } 52 | 53 | @IBOutlet weak var collectionView: UICollectionView!{ 54 | didSet{ 55 | collectionView.alwaysBounceVertical = true 56 | collectionView.dataSource = self 57 | collectionView.delegate = self 58 | collectionView.register(UINib(nibName: "StratchyHeader", bundle: nil), forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: firstHeaderReuseIdentifier) 59 | collectionView.register(UINib(nibName: "SeasonHeader", bundle: nil), forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: otherHeadersReuseIdentifier) 60 | collectionView.register(UINib(nibName: "EpisodeCell", bundle: nil), forCellWithReuseIdentifier: episodeCellReuseIdentifier) 61 | layout = collectionView.collectionViewLayout as? StratchyHeaderLayout 62 | } 63 | } 64 | 65 | // MARK: - View Life Cycle 66 | 67 | override func viewDidLoad() { 68 | super.viewDidLoad() 69 | 70 | configureFavoriteBarButton() 71 | 72 | let longPress = UILongPressGestureRecognizer(target: self, action: #selector(BaseDetailsViewController.longPress(_:))) 73 | longPress.minimumPressDuration = 0.5 74 | longPress.delaysTouchesBegan = true 75 | collectionView.addGestureRecognizer(longPress) 76 | } 77 | 78 | final func longPress(_ gesture: UILongPressGestureRecognizer) { 79 | if gesture.state == .ended { 80 | return 81 | } 82 | let p = gesture.location(in: collectionView) 83 | if let indexPath = collectionView.indexPathForItem(at: p) { 84 | if let cell = collectionView.cellForItem(at: indexPath) { 85 | cellWasLongPressed(cell, seasonIndex: indexPath.section - 1, episodeIndex: indexPath.item) 86 | } 87 | } 88 | } 89 | 90 | func configureFavoriteBarButton() { 91 | if (item.isFavorite) { 92 | self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage.removeFromFavoritesImage(), 93 | style: .done, target: self, action: #selector(BaseDetailsViewController.removeFromFavorites)) 94 | } else { 95 | self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage.addToFavoritesImage(), 96 | style: .done, target: self, action: #selector(BaseDetailsViewController.addToFavorites)) 97 | } 98 | } 99 | 100 | override func viewDidLayoutSubviews() { 101 | super.viewDidLayoutSubviews() 102 | 103 | // update header size 104 | header?.headerSize = headerSize 105 | layout?.headerSize = headerSize 106 | } 107 | 108 | // MARK: - Favorites 109 | func addToFavorites() { 110 | DataManager.sharedManager().addToFavorites(item) 111 | configureFavoriteBarButton() 112 | } 113 | 114 | func removeFromFavorites() { 115 | DataManager.sharedManager().removeFromFavorites(item) 116 | configureFavoriteBarButton() 117 | } 118 | 119 | // MARK: - BaseDetailsViewController 120 | func reloadData() { 121 | 122 | } 123 | 124 | func startPlayback(_ episode: Episode, basicInfo: BasicInfo, magnetLink: String, loadingTitle: String) { 125 | 126 | let loadingVC = self.storyboard?.instantiateViewController(withIdentifier: "loadingViewController") as! LoadingViewController 127 | loadingVC.delegate = self 128 | loadingVC.status = "Downloading..." 129 | loadingVC.loadingTitle = loadingTitle 130 | loadingVC.modalPresentationStyle = UIModalPresentationStyle.overCurrentContext 131 | self.tabBarController?.present(loadingVC, animated: true, completion: nil) 132 | 133 | PTTorrentStreamer.shared().startStreaming(fromFileOrMagnetLink: magnetLink, progress: { (status) -> Void in 134 | 135 | loadingVC.progress = status.bufferingProgress 136 | loadingVC.speed = Int(status.downloadSpeed) 137 | loadingVC.seeds = Int(status.seeds) 138 | loadingVC.peers = Int(status.peers) 139 | 140 | }, readyToPlay: { (url) -> Void in 141 | loadingVC.dismiss(animated: false, completion: nil) 142 | 143 | let vdl = VDLPlaybackViewController(nibName: "VDLPlaybackViewController", bundle: nil) 144 | vdl.delegate = self 145 | self.navigationController?.present(vdl, animated: true, completion: nil) 146 | vdl.playMedia(from: url) 147 | 148 | }, failure: { (error) -> Void in 149 | loadingVC.dismiss(animated: true, completion: nil) 150 | }) 151 | } 152 | 153 | // MARK: - VDLPlaybackViewControllerDelegate 154 | 155 | func playbackControllerDidFinishPlayback(_ playbackController: VDLPlaybackViewController!) { 156 | self.navigationController?.dismiss(animated: true, completion: nil) 157 | PTTorrentStreamer.shared().cancelStreaming() 158 | } 159 | 160 | // MARK: - LoadingViewControllerDelegate 161 | 162 | func didCancelLoading(_ controller: LoadingViewController) { 163 | PTTorrentStreamer.shared().cancelStreaming() 164 | controller.dismiss(animated: true, completion: nil) 165 | } 166 | 167 | // MARK: - UICollectionViewDataSource 168 | 169 | final func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 170 | switch section { 171 | case 0: return 0 172 | default: 173 | let seasonIndex = section - 1 174 | return self.numberOfEpisodesInSeason(seasonIndex) 175 | } 176 | } 177 | 178 | final func numberOfSections(in collectionView: UICollectionView) -> Int { 179 | return self.numberOfSeasons() + 1 // extra section for header 180 | } 181 | 182 | final func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 183 | let seasonIndex = indexPath.section - 1 184 | let episode = indexPath.item 185 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellReuseIdentifier, for: indexPath) as! EpisodeCell 186 | self.setupCell(cell, seasonIndex: seasonIndex, episodeIndex: episode) 187 | return cell 188 | } 189 | 190 | final func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { 191 | switch section { 192 | case 0: return headerSize 193 | default : return CGSize(width: collectionView.bounds.width, height: preferedOtherHeadersHeight) 194 | } 195 | } 196 | 197 | func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { 198 | if indexPath.section == 0 { 199 | if (header == nil){ 200 | header = (collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: firstHeaderReuseIdentifier, for: indexPath) as! StratchyHeader) 201 | header?.delegate = layout 202 | 203 | if let image = item.bigImage?.image { 204 | header?.image = image 205 | } else { 206 | ImageProvider.sharedInstance.imageFromURL(URL: item.bigImage?.URL) { (downloadedImage) -> () in 207 | self.item.bigImage?.image = downloadedImage 208 | self.header?.image = downloadedImage 209 | } 210 | } 211 | 212 | if let image = item.smallImage?.image { 213 | header?.foregroundImage.image = image 214 | } else { 215 | ImageProvider.sharedInstance.imageFromURL(URL: item.smallImage?.URL) { (downloadedImage) -> () in 216 | self.item.smallImage?.image = downloadedImage 217 | self.header?.foregroundImage.image = downloadedImage 218 | } 219 | } 220 | } 221 | header!.synopsisTextView.text = item.synopsis 222 | return header! 223 | } else { 224 | let otherHeader = (collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: otherHeadersReuseIdentifier, for: indexPath) as! SeasonHeader) 225 | let seasonIndex = (indexPath.section - 1) 226 | self.setupSeasonHeader(otherHeader, seasonIndex: seasonIndex) 227 | return otherHeader 228 | } 229 | } 230 | 231 | // MARK: - UICollectionViewDelegate 232 | 233 | final func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 234 | if let cell = collectionView.cellForItem(at: indexPath) { 235 | cellWasPressed(cell, seasonIndex: indexPath.section - 1, episodeIndex: indexPath.item) 236 | } 237 | } 238 | 239 | func showVideoPickerPopupForEpisode(_ episode: Episode, basicInfo: BasicInfo, fromView view: UIView) { 240 | let videos = episode.videos 241 | if (videos.count > 0) { 242 | 243 | let actionSheetController = UIAlertController(title: episode.title, message: episode.desc, preferredStyle: UIAlertControllerStyle.actionSheet) 244 | 245 | let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) 246 | actionSheetController.addAction(cancelAction) 247 | 248 | for video in videos { 249 | var title = "" 250 | if let subGroup = video.subGroup { 251 | title += "[\(subGroup)] " 252 | } 253 | if let quality = video.quality { 254 | title += quality 255 | } 256 | 257 | let action = UIAlertAction(title: title, style: UIAlertActionStyle.default, handler: { (action) -> Void in 258 | let magnetLink = video.magnetLink 259 | let episodeTitle = episode.title ?? "" 260 | let loadingTitle = "\(episodeTitle) - \(title)" 261 | self.startPlayback(episode, basicInfo: basicInfo , magnetLink: magnetLink, loadingTitle: loadingTitle) 262 | }) 263 | 264 | actionSheetController.addAction(action) 265 | } 266 | 267 | let popOver = actionSheetController.popoverPresentationController 268 | popOver?.sourceView = view 269 | popOver?.sourceRect = view.bounds 270 | popOver?.permittedArrowDirections = UIPopoverArrowDirection.any 271 | 272 | self.present(actionSheetController, animated: true, completion: nil) 273 | } 274 | } 275 | 276 | // MARK: - DetailViewControllerDataSource 277 | func numberOfSeasons() -> Int { 278 | assertionFailure("Should be overriden by subclass") 279 | return 0 280 | } 281 | 282 | func numberOfEpisodesInSeason(_ seasonsIndex: Int) -> Int { 283 | assertionFailure("Should be overriden by subclass") 284 | return 0 285 | } 286 | 287 | func setupCell(_ cell: EpisodeCell, seasonIndex: Int, episodeIndex: Int) { 288 | assertionFailure("Should be overriden by subclass") 289 | } 290 | 291 | func setupSeasonHeader(_ header: SeasonHeader, seasonIndex: Int) { 292 | assertionFailure("Should be overriden by subclass") 293 | } 294 | 295 | func cellWasPressed(_ cell: UICollectionViewCell, seasonIndex: Int, episodeIndex: Int) { 296 | assertionFailure("Should be overriden by subclass") 297 | } 298 | 299 | func cellWasLongPressed(_ cell: UICollectionViewCell, seasonIndex: Int, episodeIndex: Int) { 300 | 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /PopcornTime/Controllers/ColorfullTabBarController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorfullTabBarController.swift 3 | // PopcornTime 4 | // 5 | // Created by Andrew K. on 4/10/15. 6 | // Copyright (c) 2015 PopcornTime. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | 12 | class ColorfullTabBarController: UITabBarController, UITabBarControllerDelegate { 13 | 14 | fileprivate struct ColorConstants { 15 | static let favoritesTintColor = UIColor(red: 235/255, green: 66/255, blue: 69/255, alpha: 1.0) 16 | static let moviesTintColor = UIColor(red: 66/255, green: 166/255, blue: 235/255, alpha: 1.0) 17 | static let showsTintColor = UIColor(red: 33/255, green: 181/255, blue: 42/255, alpha: 1.0) 18 | static let animeTintColor = UIColor(red: 235/255, green: 66/255, blue: 164/255, alpha: 1.0) 19 | } 20 | 21 | override func viewDidLoad() { 22 | super.viewDidLoad() 23 | self.delegate = self 24 | } 25 | 26 | deinit { 27 | NotificationCenter.default.removeObserver(self) 28 | } 29 | 30 | override func viewWillAppear(_ animated: Bool) { 31 | super.viewWillAppear(animated) 32 | assignColors() 33 | } 34 | 35 | override func viewDidAppear(_ animated: Bool) { 36 | super.viewDidAppear(animated) 37 | assignColors() 38 | } 39 | 40 | fileprivate func assignColors() { 41 | switch selectedIndex { 42 | case 0: view.window?.tintColor = ColorConstants.favoritesTintColor 43 | case 1: view.window?.tintColor = ColorConstants.moviesTintColor 44 | case 2: view.window?.tintColor = ColorConstants.showsTintColor 45 | case 3: view.window?.tintColor = ColorConstants.animeTintColor 46 | default: break 47 | } 48 | } 49 | 50 | 51 | // MARK: - UITabBarControllerDelegate 52 | 53 | func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) { 54 | self.assignColors() 55 | } 56 | 57 | 58 | } 59 | -------------------------------------------------------------------------------- /PopcornTime/Controllers/FavoritesViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FavoritesViewController.swift 3 | // PopcornTime 4 | // 5 | // Created by Danylo Kostyshyn on 3/13/15. 6 | // Copyright (c) 2015 PopcornTime. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class FavoritesViewController: PagedViewController { 12 | 13 | override func reloadData() { 14 | if let favoriteItems = DataManager.sharedManager().favorites { 15 | self.items = favoriteItems 16 | self.collectionView?.reloadData() 17 | } 18 | 19 | } 20 | 21 | override func loadMore() { 22 | 23 | } 24 | 25 | // MARK: View Life Cycle 26 | 27 | override func viewDidLoad() { 28 | super.viewDidLoad() 29 | 30 | // No search in favorites 31 | self.searchController = nil 32 | self.navigationItem.titleView = nil 33 | 34 | NotificationCenter.default.addObserver( 35 | self, 36 | selector: #selector(FavoritesViewController.favoritesDidChange(_:)), 37 | name: NSNotification.Name(rawValue: Notifications.FavoritesDidChangeNotification), 38 | object: nil 39 | ) 40 | } 41 | 42 | deinit { 43 | NotificationCenter.default.removeObserver(self) 44 | } 45 | 46 | 47 | // MARK: - Navigation 48 | 49 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 50 | 51 | if let item = sender as? BasicInfo { 52 | 53 | if let episodesVC = segue.destination as? BaseDetailsViewController { 54 | switch item { 55 | case item as Anime: 56 | episodesVC.item = item as! Anime 57 | case item as Show: 58 | episodesVC.item = item as! Show 59 | case item as Movie: 60 | episodesVC.item = item as! Movie 61 | default: 62 | break 63 | } 64 | } 65 | } 66 | } 67 | 68 | // MARK: - Notifications 69 | 70 | func favoritesDidChange(_ notification: Notification) { 71 | self.reloadData() 72 | } 73 | 74 | // MARK: - UICollectionViewDelegate 75 | 76 | func collectionView(_ collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: IndexPath) { 77 | 78 | let item = self.items[indexPath.row] 79 | 80 | switch item { 81 | case item as Anime: 82 | performSegue(withIdentifier: "showDetailsForFavoriteAnime", sender: item) 83 | case item as Show: 84 | performSegue(withIdentifier: "showDetailsForFavoriteShow", sender: item) 85 | case item as Movie: 86 | performSegue(withIdentifier: "showDetailsForFavoriteMovie", sender: item) 87 | default: 88 | break 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /PopcornTime/Controllers/LoadingViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadingViewController.swift 3 | // PopcornTime 4 | // 5 | // Created by Danylo Kostyshyn on 3/15/15. 6 | // Copyright (c) 2015 PopcornTime. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol LoadingViewControllerDelegate { 12 | func didCancelLoading(_ controller: LoadingViewController) 13 | } 14 | 15 | class LoadingViewController: UIViewController { 16 | 17 | var delegate: LoadingViewControllerDelegate? 18 | 19 | @IBOutlet fileprivate weak var statusLabel: UILabel! 20 | @IBOutlet fileprivate weak var progressLabel: UILabel! 21 | @IBOutlet fileprivate weak var progressView: UIProgressView! 22 | @IBOutlet fileprivate weak var speedLabel: UILabel! 23 | @IBOutlet fileprivate weak var seedsLabel: UILabel! 24 | @IBOutlet fileprivate weak var peersLabel: UILabel! 25 | @IBOutlet fileprivate weak var titleLabel: UILabel! 26 | 27 | var status: String? = nil { 28 | didSet { 29 | if let status = status { 30 | statusLabel?.text = status 31 | } 32 | } 33 | } 34 | 35 | var progress: Float = 0.0 { 36 | didSet { 37 | progressView.progress = progress 38 | progressLabel.text = String(format: "%.0f%%", progress*100) 39 | } 40 | } 41 | 42 | var speed: Int = 0 { // bytes/s 43 | didSet { 44 | let formattedSpeed = ByteCountFormatter.string(fromByteCount: Int64(speed), countStyle: .binary) + "/s" 45 | speedLabel.text = String(format:"Speed: %@", formattedSpeed) 46 | } 47 | } 48 | 49 | var seeds: Int = 0 { 50 | didSet { 51 | seedsLabel.text = String(format: "Seeds: %d", seeds) 52 | } 53 | } 54 | 55 | var peers: Int = 0 { 56 | didSet { 57 | peersLabel.text = String(format: "Peers: %d", peers) 58 | } 59 | } 60 | 61 | var loadingTitle: String? = nil { 62 | didSet { 63 | if let title = loadingTitle { 64 | titleLabel?.text = title 65 | } 66 | } 67 | } 68 | 69 | // MARK: - View Life Cycle 70 | 71 | override func viewDidLoad() { 72 | super.viewDidLoad() 73 | 74 | titleLabel?.text = loadingTitle 75 | status = "Loading..." 76 | progress = 0.0 77 | speed = 0 78 | seeds = 0 79 | peers = 0 80 | 81 | UIApplication.shared.isIdleTimerDisabled = true; 82 | } 83 | 84 | override func viewDidDisappear(_ animated: Bool) { 85 | super.viewDidLoad() 86 | 87 | UIApplication.shared.isIdleTimerDisabled = false; 88 | } 89 | 90 | // MARK: - Actions 91 | 92 | @IBAction fileprivate func cancelButtonPressed(_ sender: AnyObject) { 93 | delegate?.didCancelLoading(self) 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /PopcornTime/Controllers/MovieDetailsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShowDetailsViewController.swift 3 | // PopcornTime 4 | // 5 | // Created by Andrew K. on 3/13/15. 6 | // Copyright (c) 2015 PopcornTime. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class MovieDetailsViewController: BaseDetailsViewController { 12 | 13 | var movie: Movie! { 14 | get { 15 | return self.item as! Movie 16 | } 17 | } 18 | 19 | // MARK: - UIViewController 20 | 21 | override func viewDidLoad() { 22 | super.viewDidLoad() 23 | preferedOtherHeadersHeight = 0.0 24 | } 25 | 26 | // MARK: - BaseDetailsViewController 27 | 28 | override func reloadData() { 29 | PTAPIManager.shared().showInfo(with: .movie, withId: item.identifier, success: { (item) -> Void in 30 | guard let item = item else { return } 31 | self.movie.update(item) 32 | self.collectionView?.reloadData() 33 | }, failure: nil) 34 | } 35 | 36 | // MARK: - DetailViewControllerDataSource 37 | override func numberOfSeasons() -> Int { 38 | return 1 39 | } 40 | 41 | override func numberOfEpisodesInSeason(_ seasonsIndex: Int) -> Int { 42 | return movie.videos.count 43 | } 44 | 45 | override func setupCell(_ cell: EpisodeCell, seasonIndex: Int, episodeIndex: Int) { 46 | let video = movie.videos[episodeIndex] 47 | var title = "" 48 | if let quality = video.quality { 49 | title += quality + " " 50 | } 51 | if let name = video.name { 52 | title += name 53 | } 54 | cell.titleLabel.text = title 55 | } 56 | 57 | override func setupSeasonHeader(_ header: SeasonHeader, seasonIndex: Int) { 58 | } 59 | 60 | override func cellWasPressed(_ cell: UICollectionViewCell, seasonIndex: Int, episodeIndex: Int) { 61 | let video = movie.videos[episodeIndex] 62 | let magnetLink = video.magnetLink 63 | let title = movie.title ?? "" 64 | let fakeEpisode = Episode(title: title, desc: "", seasonNumber: 0, episodeNumber: 0, videos: [Video]()) 65 | startPlayback(fakeEpisode, basicInfo: movie, magnetLink: magnetLink, loadingTitle: title) 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /PopcornTime/Controllers/MoviesViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MoviesViewController.swift 3 | // PopcornTime 4 | // 5 | // Created by Andrew K. on 3/19/15. 6 | // Copyright (c) 2015 PopcornTime. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class MoviesViewController: PagedViewController { 12 | 13 | override var showType: PTItemType { 14 | get { 15 | return .movie 16 | } 17 | } 18 | 19 | override func map(_ response: [AnyObject]) -> [BasicInfo] { 20 | let items = response.map({ Movie(dictionary: $0 as! [AnyHashable: Any]) }) as [BasicInfo] 21 | return items 22 | } 23 | 24 | func collectionView(_ collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: IndexPath) { 25 | 26 | if let cell = collectionView.cellForItem(at: indexPath){ 27 | //Check if cell is MoreShowsCell 28 | if let _ = cell as? MoreShowsCollectionViewCell{ 29 | loadMore() 30 | } else { 31 | performSegue(withIdentifier: "showDetails", sender: cell) 32 | } 33 | } 34 | } 35 | 36 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 37 | super.prepare(for: segue, sender: sender) 38 | 39 | if segue.identifier == "showDetails"{ 40 | if let episodesVC = segue.destination as? MovieDetailsViewController{ 41 | if let senderCell = sender as? UICollectionViewCell{ 42 | if let indexPath = collectionView!.indexPath(for: senderCell) { 43 | var item: BasicInfo! 44 | if (searchController!.isActive) { 45 | item = searchResults[indexPath.row] 46 | } else { 47 | item = items[indexPath.row] 48 | } 49 | episodesVC.item = item as! Movie 50 | } 51 | } 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /PopcornTime/Controllers/OAuthViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebViewController.swift 3 | // PopcornTime 4 | // 5 | // Created by Danylo Kostyshyn on 3/15/15. 6 | // Copyright (c) 2015 PopcornTime. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol OAuthViewControllerDelegate { 12 | func oauthViewControllerDidFinish(_ controller: OAuthViewController, token: String?, error: NSError?) 13 | } 14 | 15 | class OAuthViewController: UIViewController, UIWebViewDelegate { 16 | 17 | @IBOutlet weak var webView: UIWebView! 18 | @IBOutlet weak var navigationBar: UINavigationBar! 19 | var delegate: OAuthViewControllerDelegate? 20 | var URL: Foundation.URL? 21 | 22 | // MARK: - View Life Cycle 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | 27 | if let URL = URL { 28 | webView.loadRequest(URLRequest(url: URL)) 29 | } 30 | } 31 | 32 | // MARK: - UIWebViewDelegate 33 | 34 | func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebViewNavigationType) -> Bool { 35 | if let code = request.url?.lastPathComponent { 36 | if code.characters.count == 64 { 37 | // PTAPIManager.sharedManager().accessTokenWithAuthorizationCode(code, success: { (accessToken) -> Void in 38 | // println("OAuth access token: \(accessToken)") 39 | // self.delegate?.oauthViewControllerDidFinish(self, token: accessToken, error: nil) 40 | // }, failure: { (error) -> Void in 41 | // println("\(error)") 42 | // self.delegate?.oauthViewControllerDidFinish(self, token: nil, error: error) 43 | // }) 44 | } 45 | } 46 | return true; 47 | } 48 | 49 | func webViewDidStartLoad(_ webView: UIWebView) { 50 | 51 | } 52 | 53 | func webViewDidFinishLoad(_ webView: UIWebView) { 54 | 55 | } 56 | 57 | func webView(_ webView: UIWebView, didFailLoadWithError error: Error) { 58 | delegate?.oauthViewControllerDidFinish(self, token: nil, error: error as NSError?) 59 | } 60 | 61 | // MARK: - Actions 62 | 63 | @IBAction func cancelButtonPressed(_ sender: AnyObject) { 64 | delegate?.oauthViewControllerDidFinish(self, token: nil, error: nil) 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /PopcornTime/Controllers/PagedViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PagedViewController.swift 3 | // PopcornTime 4 | // 5 | // Created by Andrew K. on 3/19/15. 6 | // Copyright (c) 2015 PopcornTime. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class PagedViewController: BaseCollectionViewController, UISearchBarDelegate, UISearchResultsUpdating { 12 | 13 | fileprivate var contentPage: UInt = 0 14 | 15 | var searchResults = [BasicInfo]() 16 | var searchController: UISearchController? 17 | var searchTimer: Timer? 18 | 19 | var showType: PTItemType { 20 | get { 21 | assert(false, "this must be overriden by subclass") 22 | return .movie 23 | } 24 | } 25 | 26 | // MARK: View Life Cycle 27 | 28 | override func viewDidLoad() { 29 | super.viewDidLoad() 30 | 31 | setupSearch() 32 | } 33 | 34 | override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { 35 | super.viewWillTransition(to: size, with: coordinator) 36 | 37 | collectionViewLayout?.invalidateLayout() 38 | } 39 | 40 | fileprivate func setupSearch() { 41 | 42 | self.definesPresentationContext = true 43 | 44 | searchController = UISearchController(searchResultsController: nil) 45 | searchController!.searchResultsUpdater = self 46 | searchController!.hidesNavigationBarDuringPresentation = false 47 | searchController!.dimsBackgroundDuringPresentation = false 48 | 49 | let searchBar = searchController!.searchBar 50 | searchBar.delegate = self 51 | searchBar.barStyle = .black 52 | searchBar.backgroundImage = UIImage() 53 | 54 | 55 | let searchBarContainer = UIView(frame: navigationController!.navigationBar.bounds) 56 | searchBarContainer.addSubview(searchBar) 57 | searchBar.translatesAutoresizingMaskIntoConstraints = false 58 | navigationItem.titleView = searchBarContainer 59 | 60 | let views = ["searchBar" : searchBar] 61 | searchBarContainer.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-0-[searchBar]-0-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: views)) 62 | searchBarContainer.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-0-[searchBar]-0-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: views)) 63 | } 64 | 65 | // MARK: 66 | 67 | func map(_ response: [AnyObject]) -> [BasicInfo] { 68 | return [BasicInfo]() 69 | } 70 | 71 | override func reloadData() { 72 | PTAPIManager.shared().topShows(with: showType, withPage: contentPage, success: { (items) -> Void in 73 | self.showLoadMoreCell = true 74 | if let items = items { 75 | self.items = self.map(items as [AnyObject]) 76 | self.collectionView?.reloadData() 77 | } 78 | }, failure: nil) 79 | } 80 | 81 | func loadMore() { 82 | PTAPIManager.shared().topShows(with: showType, withPage: contentPage+1, success: { (items) -> Void in 83 | if let items = items { 84 | self.contentPage += 1 85 | let newItems = self.map(items as [AnyObject]) 86 | let newShowsIndexPathes = newItems.enumerated().map({ (index, item) in 87 | return IndexPath(row: (self.items.count + index), section: 0) 88 | }) 89 | self.items += newItems 90 | 91 | self.collectionView?.insertItems(at: newShowsIndexPathes) 92 | } 93 | }, failure: nil) 94 | } 95 | 96 | func performSearch() { 97 | let text = searchController!.searchBar.text 98 | if text!.characters.count > 0 { 99 | PTAPIManager.shared().searchForShow(with: showType, name: text, success: { (items) -> Void in 100 | self.showLoadMoreCell = false 101 | if let items = items { 102 | self.searchResults = self.map(items as [AnyObject]) 103 | } else { 104 | self.searchResults.removeAll(keepingCapacity: false) 105 | } 106 | self.collectionView?.reloadData() 107 | }, failure: nil) 108 | } 109 | } 110 | 111 | // MARK: UICollectionViewDataSource 112 | 113 | override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 114 | if searchController != nil && searchController!.isActive { 115 | return searchResults.count 116 | } 117 | return super.collectionView(collectionView, numberOfItemsInSection: section) 118 | } 119 | 120 | override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 121 | if searchController != nil && searchController!.isActive { 122 | //Ordinary show cell 123 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifierShow, for: indexPath) as! ShowCollectionViewCell 124 | 125 | let item = searchResults[indexPath.row] 126 | cell.title = item.title 127 | 128 | let imageItem = item.smallImage 129 | switch imageItem?.status { 130 | case .new?: 131 | imageItem?.status = .downloading 132 | ImageProvider.sharedInstance.imageFromURL(URL: imageItem?.URL) { (downloadedImage) -> () in 133 | imageItem?.image = downloadedImage 134 | imageItem?.status = .finished 135 | 136 | collectionView.reloadItems(at: [indexPath]) 137 | } 138 | case .finished?: 139 | cell.image = imageItem?.image 140 | default: break 141 | } 142 | 143 | return cell 144 | } 145 | return super.collectionView(collectionView, cellForItemAt: indexPath) 146 | } 147 | 148 | // MARK: UICollectionViewDelegate 149 | 150 | func collectionView(_ collectionView: UICollectionView, willDisplayCell cell: UICollectionViewCell, forItemAtIndexPath indexPath: IndexPath) { 151 | if indexPath.row == items.count { 152 | loadMore() 153 | } 154 | } 155 | 156 | // MARK: UISearchBarDelegate 157 | 158 | func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { 159 | self.searchTimer?.invalidate() 160 | self.searchTimer = nil 161 | performSearch() 162 | } 163 | 164 | func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { 165 | 166 | self.showLoadMoreCell = true 167 | 168 | self.searchTimer?.invalidate() 169 | self.searchTimer = nil 170 | 171 | searchResults.removeAll(keepingCapacity: false) 172 | self.collectionView.reloadData() 173 | } 174 | 175 | // MARK: UISearchResultsUpdating 176 | 177 | func updateSearchResults(for searchController: UISearchController) { 178 | self.searchTimer?.invalidate() 179 | self.collectionView.reloadData() 180 | self.searchTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(PagedViewController.performSearch), userInfo: nil, repeats: false) 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /PopcornTime/Controllers/ParseViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ParseViewController.swift 3 | // 4 | // 5 | // Created by Andriy K. on 6/22/15. 6 | // 7 | // 8 | 9 | import UIKit 10 | 11 | class ParseViewController: UIViewController, PFLogInViewControllerDelegate { 12 | 13 | private var canPromptLogin = true 14 | 15 | // MARK: - UIViewController 16 | 17 | override func viewDidAppear(animated: Bool) { 18 | super.viewDidAppear(animated) 19 | promptLoginIfNeeded(true) 20 | } 21 | 22 | // MARK: - Login 23 | 24 | func promptLoginIfNeeded(animated: Bool) { 25 | 26 | let currentUser = ParseManager.sharedInstance.user 27 | if currentUser == nil { 28 | // Show the signup or login screen 29 | let logInController = PFLogInViewController() 30 | logInController.delegate = self 31 | logInController.fields = 32 | [PFLogInFields.DismissButton, PFLogInFields.Facebook] 33 | logInController.facebookPermissions = ["public_profile"] 34 | self.presentViewController(logInController, animated:animated, completion: nil) 35 | } 36 | } 37 | 38 | // MARK: PFLogInViewControllerDelegate 39 | 40 | func logInViewController(logInController: PFLogInViewController, didLogInUser user: PFUser) { 41 | dissmiss(nil) 42 | } 43 | 44 | func logInViewController(logInController: PFLogInViewController, didFailToLogInWithError error: NSError?) { 45 | } 46 | 47 | func logInViewControllerDidCancelLogIn(logInController: PFLogInViewController) { 48 | dissmiss(nil) 49 | dissmiss(nil) 50 | } 51 | 52 | // MARK: - Actions 53 | 54 | @IBAction func dissmiss(sender: AnyObject?) { 55 | canPromptLogin = false 56 | self.dismissViewControllerAnimated(true, completion: nil) 57 | } 58 | 59 | @IBAction func logOutPressed(sender: UIBarButtonItem) { 60 | PFUser.logOut() 61 | dissmiss(nil) 62 | } 63 | @IBAction func clearAllDataPressed(sender: UIBarButtonItem) { 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /PopcornTime/Controllers/SettingsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsViewController.swift 3 | // PopcornTime 4 | // 5 | // Created by Danylo Kostyshyn on 4/4/15. 6 | // Copyright (c) 2015 PopcornTime. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SettingsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { 12 | 13 | // MARK: - View Life Cycle 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | } 18 | 19 | // MARK: - 20 | 21 | func appInfoString() -> String { 22 | let displayName = Bundle.main.infoDictionary?["CFBundleDisplayName"] as! String 23 | let version = Bundle.main.infoDictionary?["CFBundleVersion"] as! String 24 | let shortVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String 25 | return "\(displayName) \(shortVersion) (\(version))" 26 | } 27 | 28 | // MARK: - UITableViewDataSource 29 | 30 | func numberOfSections(in tableView: UITableView) -> Int { 31 | return 1 32 | } 33 | 34 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 35 | return 1 36 | } 37 | 38 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 39 | let identifier = "SettingsCell" 40 | var cell: UITableViewCell! = tableView.dequeueReusableCell(withIdentifier: "SettingsCell") 41 | if cell == nil { 42 | cell = UITableViewCell(style: .default, reuseIdentifier: identifier) 43 | } 44 | 45 | cell.textLabel?.text = "Hello, PopcornTime!" 46 | 47 | return cell 48 | } 49 | 50 | func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { 51 | let label = UILabel(frame: CGRect(x: 0.0, y: 0.0, width: tableView.bounds.width, height: 0.0)) 52 | label.backgroundColor = UIColor.clear 53 | label.font = UIFont.systemFont(ofSize: 14.0) 54 | label.text = appInfoString() 55 | label.textAlignment = .center 56 | return label 57 | } 58 | 59 | func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { 60 | return 30.0 61 | } 62 | 63 | // MARK: - UITableViewDelegate 64 | 65 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 66 | tableView .deselectRow(at: indexPath, animated: true) 67 | } 68 | 69 | @IBAction func doneButtonTapped(_ sender: AnyObject) { 70 | self.dismiss(animated: true, completion: nil) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /PopcornTime/Controllers/ShowDetailsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShowDetailsViewController.swift 3 | // PopcornTime 4 | // 5 | // Created by Andrew K. on 3/13/15. 6 | // Copyright (c) 2015 PopcornTime. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ShowDetailsViewController: BaseDetailsViewController { 12 | 13 | var show: Show! { 14 | get { 15 | return self.item as! Show 16 | } 17 | } 18 | 19 | // MARK: - BaseDetailsViewController 20 | 21 | override func reloadData() { 22 | PTAPIManager.shared().showInfo(with: .show, withId: show.identifier, success: { (item) -> Void in 23 | guard let item = item else { return } 24 | self.show.update(item) 25 | self.collectionView?.reloadData() 26 | }, failure: nil) 27 | } 28 | 29 | // MARK: - DetailViewControllerDataSource 30 | override func numberOfSeasons() -> Int { 31 | return show.seasons.count 32 | } 33 | 34 | override func numberOfEpisodesInSeason(_ seasonsIndex: Int) -> Int { 35 | return show.seasons[seasonsIndex].episodes.count 36 | } 37 | 38 | override func setupCell(_ cell: EpisodeCell, seasonIndex: Int, episodeIndex: Int) { 39 | let episode = show.seasons[seasonIndex].episodes[episodeIndex] 40 | if let title = episode.title { 41 | cell.titleLabel.text = "S\(episode.seasonNumber)E\(episode.episodeNumber): \(title)" 42 | } else { 43 | cell.titleLabel.text = "S\(episode.seasonNumber)E\(episode.episodeNumber)" 44 | } 45 | } 46 | 47 | override func setupSeasonHeader(_ header: SeasonHeader, seasonIndex: Int) { 48 | let seasonNumber = self.show.seasons[seasonIndex].seasonNumber 49 | header.titleLabel.text = "Season \(seasonNumber)" 50 | } 51 | 52 | override func cellWasPressed(_ cell: UICollectionViewCell, seasonIndex: Int, episodeIndex: Int) { 53 | let episode = show.episodeFor(seasonIndex: seasonIndex, episodeIndex: episodeIndex) 54 | showVideoPickerPopupForEpisode(episode, basicInfo: self.item, fromView: cell) 55 | } 56 | 57 | override func cellWasLongPressed(_ cell: UICollectionViewCell, seasonIndex: Int, episodeIndex: Int) { 58 | // let episode = show.episodeFor(seasonIndex: seasonIndex, episodeIndex: episodeIndex) 59 | // let seasonEpisodes = show.episodesFor(seasonIndex: seasonIndex) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /PopcornTime/Controllers/ShowsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TVSeriesShowsViewController.swift 3 | // PopcornTime 4 | // 5 | // Created by Andrew K. on 3/9/15. 6 | // Copyright (c) 2015 PopcornTime. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ShowsViewController: PagedViewController { 12 | 13 | override var showType: PTItemType { 14 | get { 15 | return .show 16 | } 17 | } 18 | 19 | override func map(_ response: [AnyObject]) -> [BasicInfo] { 20 | return response.map({ Show(dictionary: $0 as! [AnyHashable: Any]) }) 21 | } 22 | 23 | func collectionView(_ collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: IndexPath) { 24 | 25 | if let cell = collectionView.cellForItem(at: indexPath){ 26 | //Check if cell is MoreShowsCell 27 | if let _ = cell as? MoreShowsCollectionViewCell { 28 | loadMore() 29 | } else { 30 | performSegue(withIdentifier: "showDetails", sender: cell) 31 | } 32 | } 33 | } 34 | 35 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 36 | super.prepare(for: segue, sender: sender) 37 | 38 | if segue.identifier == "showDetails" { 39 | if let episodesVC = segue.destination as? ShowDetailsViewController { 40 | if let senderCell = sender as? UICollectionViewCell { 41 | if let indexPath = collectionView!.indexPath(for: senderCell) { 42 | var item: BasicInfo! 43 | if (searchController!.isActive) { 44 | item = searchResults[indexPath.row] 45 | } else { 46 | item = items[indexPath.row] 47 | } 48 | episodesVC.item = item as! Show 49 | } 50 | } 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /PopcornTime/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | Popcorn Time 9 | CFBundleExecutable 10 | ${EXECUTABLE_NAME} 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | ${PRODUCT_NAME} 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1.0 25 | Fabric 26 | 27 | APIKey 28 | API_KEY 29 | Kits 30 | 31 | 32 | KitInfo 33 | 34 | KitName 35 | Crashlytics 36 | 37 | 38 | 39 | LSRequiresIPhoneOS 40 | 41 | NSAppTransportSecurity 42 | 43 | NSAllowsArbitraryLoads 44 | 45 | 46 | UILaunchStoryboardName 47 | Launch Screen 48 | UIMainStoryboardFile 49 | Main 50 | UIRequiredDeviceCapabilities 51 | 52 | UIStatusBarHidden 53 | 54 | UIStatusBarStyle 55 | UIStatusBarStyleBlackOpaque 56 | UISupportedInterfaceOrientations 57 | 58 | UIInterfaceOrientationPortrait 59 | UIInterfaceOrientationLandscapeLeft 60 | UIInterfaceOrientationLandscapeRight 61 | 62 | UIViewControllerBasedStatusBarAppearance 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /PopcornTime/Models/APIManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIManager.swift 3 | // popcornTime 4 | // 5 | // Created by Danylo Kostyshyn on 3/13/15. 6 | // Copyright (c) 2015 Danylo Kostyshyn. All rights reserved. 7 | // 8 | 9 | /* 10 | http://ytspt.re/api/list.json?limit=30&order=desc&sort=seeds 11 | http://ytspt.re/api/listimdb.json?imdb_id=tt2245084 12 | http://ytspt.re/api/list.json?limit=30&keywords=terminator&order=desc&sort=seeds&set=1 13 | http://www.yifysubtitles.com//subtitle-api/big-hero-6-yify-36523.zip 14 | 15 | http://eztvapi.re/shows/1?limit=30&order=desc&sort=seeds 16 | http://eztvapi.re/show/tt0898266 17 | http://eztvapi.re/shows/1?limit=30&keywords=the+big+bang&order=desc&sort=seeds 18 | 19 | http://ptp.haruhichan.com/list.php? 20 | http://ptp.haruhichan.com/anime.php?id=912 21 | */ 22 | 23 | import Foundation 24 | 25 | class APIManager { 26 | 27 | typealias APIManagerFailure = (error: NSError?) -> () 28 | typealias APIManagerSuccessItems = (items: [AnyObject]?) -> () 29 | typealias APIManagerSuccessItem = (item: [String: AnyObject]?) -> () 30 | 31 | private let APIManagerMoviesEndPoint = "http://ytspt.re/api" 32 | private let APIManagerShowsEndPoint = "http://eztvapi.re" 33 | private let APIManagerResultsLimit = 30 34 | 35 | class func sharedManager() -> APIManager { 36 | struct Static { static let instance: APIManager = APIManager() } 37 | return Static.instance 38 | } 39 | 40 | private func data(url: NSURL, sucess: ((AnyObject?) -> ())?, failure: APIManagerFailure?) { 41 | NSURLSession.sharedSession().dataTaskWithURL(url, completionHandler: { 42 | (data, response, error) -> Void in 43 | 44 | var serializationError: NSError? 45 | var JSONObject: AnyObject? = NSJSONSerialization.JSONObjectWithData(data, options: nil, error: &serializationError) 46 | 47 | if let serializationError = serializationError { 48 | println("\(serializationError)") 49 | 50 | if let failure = failure { 51 | failure(error: serializationError) 52 | } 53 | } 54 | 55 | if let sucess = sucess { 56 | sucess(JSONObject) 57 | } 58 | 59 | }).resume() 60 | } 61 | 62 | // MARK: Movies 63 | 64 | func topMovies(success: APIManagerSuccessItems?, failure: APIManagerFailure?) { 65 | var path = String(format: "list.json?limit=%d&order=desc&sort=seeds", APIManagerResultsLimit) 66 | var url = NSURL(string: APIManagerMoviesEndPoint.stringByAppendingPathComponent(path)) 67 | 68 | data(url!, sucess: { (JSONObject) -> () in 69 | if let success = success { 70 | var dict = JSONObject as [String: AnyObject] 71 | success(items: dict["MovieList"] as [AnyObject]?) 72 | } 73 | }, failure: failure) 74 | } 75 | 76 | func movieInfo(imdbId: String, success: APIManagerSuccessItems?, failure: APIManagerFailure?) { 77 | var path = String(format: "listimdb.json?imdb_id=%@", imdbId) 78 | var url = NSURL(string: APIManagerMoviesEndPoint.stringByAppendingPathComponent(path)) 79 | 80 | data(url!, sucess: { (JSONObject) -> () in 81 | if let success = success { 82 | var dict = JSONObject as [String: AnyObject] 83 | success(items: dict["MovieList"] as [AnyObject]?) 84 | } 85 | }, failure: failure) 86 | } 87 | 88 | func searchMovie(name: String, success: APIManagerSuccessItems?, failure: APIManagerFailure?) { 89 | var path = String(format: "list.json?limit=%lu&keywords=%@&order=desc&sort=seeds&set=1", APIManagerResultsLimit, name) 90 | var url = NSURL(string: APIManagerMoviesEndPoint.stringByAppendingPathComponent(path)) 91 | 92 | data(url!, sucess: { (JSONObject) -> () in 93 | if let success = success { 94 | var dict = JSONObject as [String: AnyObject] 95 | success(items: dict["MovieList"] as [AnyObject]?) 96 | } 97 | }, failure: failure) 98 | } 99 | 100 | // MARK: Shows 101 | 102 | func topShows(page: UInt, success: APIManagerSuccessItems?, failure: APIManagerFailure?) { 103 | var path = String(format: "shows/%lu?limit=%lu&order=desc&sort=seeds", (page + 1), APIManagerResultsLimit) 104 | var url = NSURL(string: APIManagerShowsEndPoint.stringByAppendingPathComponent(path)) 105 | 106 | data(url!, sucess: { (JSONObject) -> () in 107 | if let success = success { 108 | success(items: JSONObject as [AnyObject]?) 109 | } 110 | }, failure: failure) 111 | } 112 | 113 | func showInfo(imdbId: String, success: APIManagerSuccessItem?, failure: APIManagerFailure?) { 114 | var path = String(format: "show/%@", imdbId) 115 | var url = NSURL(string: APIManagerShowsEndPoint.stringByAppendingPathComponent(path)) 116 | 117 | data(url!, sucess: { (JSONObject) -> () in 118 | if let success = success { 119 | success(item: JSONObject as [String: AnyObject]?) 120 | } 121 | }, failure: failure) 122 | } 123 | 124 | func searchShow(name: String, success: APIManagerSuccessItems?, failure: APIManagerFailure?) { 125 | var path = String(format: "shows/1?limit=%lu&keywords=%@&sort=seeds", APIManagerResultsLimit, name) 126 | var url = NSURL(string: APIManagerShowsEndPoint.stringByAppendingPathComponent(path)) 127 | 128 | data(url!, sucess: { (JSONObject) -> () in 129 | if let success = success { 130 | success(items: JSONObject as [AnyObject]?) 131 | } 132 | }, failure: failure) 133 | } 134 | 135 | } -------------------------------------------------------------------------------- /PopcornTime/Models/Anime.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Anime.swift 3 | // PopcornTime 4 | // 5 | // Created by Danylo Kostyshyn on 3/19/15. 6 | // Copyright (c) 2015 PopcornTime. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class Anime: BasicInfo { 12 | var seasons = [Season]() 13 | 14 | required init(dictionary: [AnyHashable: Any]) { 15 | super.init(dictionary: dictionary) 16 | 17 | let id = dictionary["id"] as! Int 18 | identifier = "\(id)" 19 | title = dictionary["name"] as? String 20 | year = dictionary["name"] as? String 21 | 22 | if let poster = dictionary["malimg"] as? String { 23 | images = [Image]() 24 | 25 | let URL = Foundation.URL(string: poster) 26 | let image = Image(URL: URL!, type: .poster) 27 | images.append(image) 28 | } 29 | 30 | smallImage = images.filter({$0.type == ImageType.poster}).first 31 | bigImage = smallImage 32 | } 33 | 34 | required init(coder aDecoder: NSCoder) { 35 | super.init(coder: aDecoder)! 36 | } 37 | 38 | override func update(_ dictionary: [AnyHashable: Any]) { 39 | seasons.removeAll(keepingCapacity: true) 40 | 41 | let episodesDicts = dictionary["episodes"] as! [[AnyHashable: Any]] 42 | let seasonNumber:UInt = 0 43 | 44 | var videosContainer = [Int: [Video]]() 45 | var episodesContainer = [Int: Episode]() 46 | synopsis = dictionary["synopsis"] as? String 47 | if let sps = synopsis { 48 | synopsis = sps.replacingOccurrences(of: "oatRightHeader\">EditSynopsis\n", with: "") 49 | } 50 | 51 | 52 | for episodeDict in episodesDicts{ 53 | let title = episodeDict["name"] as! String 54 | let numbersFromTitle = numbersFromAnimeTitle(title) 55 | let synopsis = episodeDict["overview"] as? String 56 | if numbersFromTitle.count > 0 { 57 | if let quality = episodeDict["quality"] as? String{ 58 | // Get entry data 59 | let subGroup = episodeDict["subgroup"] as? String 60 | let episodeNumber = numbersFromTitle.first! 61 | let magnetLink = episodeDict["magnet"] as! String 62 | let video = Video(name: title, quality: quality, size: 0, duration: 0, subGroup: subGroup, magnetLink: magnetLink) 63 | 64 | var videos = videosContainer[episodeNumber] 65 | if (videos == nil) { 66 | videos = [Video]() 67 | videosContainer[episodeNumber] = videos 68 | } 69 | videosContainer[episodeNumber]!.append(video) 70 | 71 | 72 | var episode = episodesContainer[episodeNumber] 73 | if (episode == nil) { 74 | episode = Episode(title: title, desc: synopsis, seasonNumber: seasonNumber, episodeNumber: UInt(episodeNumber), videos: [Video]()) 75 | episodesContainer[episodeNumber] = episode! 76 | } 77 | } 78 | } 79 | } 80 | 81 | for entry in videosContainer { 82 | let episodeNumber = entry.0 83 | let videos = entry.1 84 | 85 | episodesContainer[episodeNumber]!.videos = videos 86 | } 87 | 88 | 89 | let episodes = Array(episodesContainer.values).sorted { (a, b) -> Bool in 90 | return a.episodeNumber > b.episodeNumber 91 | } 92 | 93 | let season = Season(seasonNumber: seasonNumber, episodes: episodes) 94 | seasons.append(season) 95 | } 96 | 97 | fileprivate func numbersFromAnimeTitle(_ title: String) -> [Int]{ 98 | let components = title.components(separatedBy: CharacterSet(charactersIn: "[]_() ")) 99 | var numbers = [Int]() 100 | for component in components { 101 | if let number = Int(component) { 102 | if number < 10000 { 103 | numbers.append(number) 104 | } 105 | } 106 | } 107 | return numbers 108 | } 109 | } 110 | 111 | extension Anime: ContainsEpisodes { 112 | func episodeFor(seasonIndex: Int, episodeIndex: Int) -> Episode { 113 | let episode = seasons[seasonIndex].episodes[episodeIndex] 114 | return episode 115 | } 116 | 117 | func episodesFor(seasonIndex: Int) -> [Episode] { 118 | return seasons[seasonIndex].episodes 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /PopcornTime/Models/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // PopcornTime 4 | // 5 | // Created by Danylo Kostyshyn on 3/13/15. 6 | // Copyright (c) 2015 PopcornTime. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | #if RELEASE 12 | import Fabric 13 | import Crashlytics 14 | #endif 15 | 16 | @UIApplicationMain 17 | class AppDelegate: UIResponder, UIApplicationDelegate { 18 | 19 | var window: UIWindow? 20 | 21 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 22 | #if RELEASE 23 | Fabric.with([Crashlytics()]) 24 | #endif 25 | return true 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /PopcornTime/Models/BaseStructures.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Video.swift 3 | // PopcornTime 4 | // 5 | // Created by Danylo Kostyshyn on 3/21/15. 6 | // Copyright (c) 2015 PopcornTime. All rights reserved. 7 | // 8 | 9 | struct Video { 10 | let name: String? 11 | let quality: String? 12 | let size: UInt? 13 | let duration: UInt? 14 | let subGroup: String? 15 | let magnetLink: String 16 | } 17 | 18 | struct Episode { 19 | let title: String? 20 | let desc: String? 21 | let seasonNumber: UInt 22 | let episodeNumber: UInt 23 | var videos = [Video]() 24 | } 25 | 26 | struct Season { 27 | let seasonNumber: UInt 28 | let episodes: [Episode] 29 | } 30 | -------------------------------------------------------------------------------- /PopcornTime/Models/BasicInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BasicInfo.swift 3 | // PopcornTime 4 | // 5 | // Created by Danylo Kostyshyn on 3/19/15. 6 | // Copyright (c) 2015 PopcornTime. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol ContainsEpisodes { 12 | func episodeFor(seasonIndex: Int, episodeIndex: Int) -> Episode 13 | func episodesFor(seasonIndex: Int) -> [Episode] 14 | } 15 | 16 | protocol BasicInfoProtocol { 17 | var identifier: String! {get} 18 | var title: String? {get} 19 | var year: String? {get} 20 | var images: [Image]! {get} 21 | var smallImage: Image? {get} 22 | var bigImage: Image? {get} 23 | var isFavorite: Bool {get} 24 | 25 | init(dictionary: [AnyHashable: Any]) 26 | func update(_ dictionary: [AnyHashable: Any]) 27 | } 28 | 29 | class BasicInfo: NSObject, BasicInfoProtocol, NSCoding { 30 | var identifier: String! 31 | var title: String? 32 | var year: String? 33 | var images: [Image]! 34 | var smallImage: Image? 35 | var bigImage: Image? 36 | var synopsis: String? 37 | 38 | var isFavorite : Bool { 39 | get { 40 | return DataManager.sharedManager().isFavorite(self) 41 | } 42 | set { 43 | if (isFavorite == true) { 44 | return DataManager.sharedManager().addToFavorites(self) 45 | } else { 46 | return DataManager.sharedManager().removeFromFavorites(self) 47 | } 48 | } 49 | } 50 | 51 | required init(dictionary: [AnyHashable: Any]) { 52 | // fatalError("init(dictionary:) has not been implemented") 53 | } 54 | 55 | func update(_ dictionary: [AnyHashable: Any]) { 56 | fatalError("update(dictionary:) has not been implemented") 57 | } 58 | 59 | // MARK: - NSCoding 60 | 61 | required init?(coder aDecoder: NSCoder) { 62 | guard 63 | let identifier = aDecoder.decodeObject(forKey: "identifier") as? String 64 | else { return nil } 65 | 66 | self.identifier = identifier 67 | self.title = aDecoder.decodeObject(forKey: "title") as? String 68 | self.year = aDecoder.decodeObject(forKey: "year") as? String 69 | self.smallImage = aDecoder.decodeObject(forKey: "smallImage") as? Image 70 | self.bigImage = aDecoder.decodeObject(forKey: "bigImage") as? Image 71 | } 72 | 73 | func encode(with aCoder: NSCoder) { 74 | aCoder.encode(identifier, forKey: "identifier") 75 | aCoder.encode(title, forKey: "title") 76 | aCoder.encode(year, forKey: "year") 77 | aCoder.encode(smallImage, forKey: "smallImage") 78 | aCoder.encode(bigImage, forKey: "bigImage") 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /PopcornTime/Models/DataManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataManager.swift 3 | // PopcornTime 4 | // 5 | // Created by Danylo Kostyshyn on 3/24/15. 6 | // Copyright (c) 2015 PopcornTime. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Notifications { 12 | static let FavoritesDidChangeNotification = "FavoritesDidChangeNotification" 13 | } 14 | 15 | class DataManager { 16 | let documentsDirectory = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first! as String 17 | let fileName = "Favorites.plist" 18 | var filePath: String { 19 | get { 20 | return fileURL.path 21 | } 22 | } 23 | var fileURL: URL { 24 | let url = URL(fileURLWithPath: documentsDirectory, isDirectory: true) 25 | return url.appendingPathComponent(fileName) 26 | } 27 | var favorites: [BasicInfo]? 28 | 29 | init() { 30 | loadFavorites() 31 | } 32 | 33 | class func sharedManager() -> DataManager { 34 | struct Static { static let instance: DataManager = DataManager() } 35 | return Static.instance 36 | } 37 | 38 | fileprivate func loadFavorites() -> [BasicInfo]? { 39 | if FileManager.default.fileExists(atPath: filePath) { 40 | let data = try? Data(contentsOf: URL(fileURLWithPath: filePath)) 41 | if let data = data { 42 | self.favorites = NSKeyedUnarchiver.unarchiveObject(with: data) as! [BasicInfo]? 43 | return self.favorites 44 | } 45 | } 46 | return nil 47 | } 48 | 49 | fileprivate func saveFavorites(_ items: [BasicInfo]) { 50 | let data = NSKeyedArchiver.archivedData(withRootObject: items) 51 | try? data.write(to: URL(fileURLWithPath: filePath), options: [.atomic]) 52 | self.favorites = items 53 | } 54 | 55 | // MARK: - 56 | 57 | func isFavorite(_ item: BasicInfo) -> Bool { 58 | let favoriteItem = self.favorites?.filter({ $0.identifier == item.identifier }).first 59 | if favoriteItem != nil { 60 | return true 61 | } 62 | return false 63 | } 64 | 65 | func addToFavorites(_ item: BasicInfo) { 66 | var items = [BasicInfo]() 67 | if let favorites = loadFavorites() { 68 | items += favorites 69 | } 70 | items.append(item) 71 | saveFavorites(items) 72 | 73 | NotificationCenter.default.post(name: Notification.Name(rawValue: Notifications.FavoritesDidChangeNotification), object: nil) 74 | } 75 | 76 | func removeFromFavorites(_ item: BasicInfo) { 77 | let items = loadFavorites() 78 | if var items = items { 79 | let favoriteItem = items.filter({ $0.identifier == item.identifier }).first 80 | if let favoriteItem = favoriteItem { 81 | let idx = items.index(of: favoriteItem) 82 | items.remove(at: idx!) 83 | saveFavorites(items) 84 | 85 | NotificationCenter.default.post(name: Notification.Name(rawValue: Notifications.FavoritesDidChangeNotification), object: nil) 86 | } 87 | } 88 | } 89 | } 90 | 91 | 92 | -------------------------------------------------------------------------------- /PopcornTime/Models/Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+PopcornTime.swift 3 | // PopcornTime 4 | // 5 | // Created by Danylo Kostyshyn on 3/30/15. 6 | // Copyright (c) 2015 PopcornTime. All rights reserved. 7 | // 8 | 9 | extension UIImage { 10 | 11 | class func addToFavoritesImage() -> UIImage? { 12 | return UIImage(named: "AddToFavoritesIcon") 13 | } 14 | 15 | class func removeFromFavoritesImage() -> UIImage? { 16 | return UIImage(named: "RemoveFromFavoritesIcon") 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /PopcornTime/Models/Image.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // PopcornTime 4 | // 5 | // Created by Danylo Kostyshyn on 3/21/15. 6 | // Copyright (c) 2015 PopcornTime. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | enum ImageType: Int { 12 | case banner, poster, fanart 13 | } 14 | 15 | enum ImageStatus { 16 | case new, downloading, finished 17 | } 18 | 19 | class Image: NSObject, NSCoding { 20 | let URL: Foundation.URL 21 | var image: UIImage? 22 | let type: ImageType 23 | var status: ImageStatus = .new 24 | 25 | init(URL: Foundation.URL, type: ImageType) { 26 | self.URL = URL 27 | self.type = type 28 | } 29 | 30 | // MARK: - NSCoding 31 | 32 | required init?(coder aDecoder: NSCoder) { 33 | let typeRaw = aDecoder.decodeInteger(forKey: "type") 34 | guard 35 | let URL = aDecoder.decodeObject(forKey: "URL") as? Foundation.URL, 36 | let type = ImageType(rawValue: typeRaw) 37 | else { return nil } 38 | 39 | self.URL = URL 40 | self.type = type 41 | } 42 | 43 | func encode(with aCoder: NSCoder) { 44 | aCoder.encode(URL, forKey: "URL") 45 | aCoder.encode(type.rawValue, forKey: "type") 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /PopcornTime/Models/ImageProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageProvider.swift 3 | // PopcornTime 4 | // 5 | // Created by Andrew K. on 3/10/15. 6 | // Copyright (c) 2015 PopcornTime. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ImageProvider: NSObject { 12 | 13 | static let sharedInstance = ImageProvider() 14 | 15 | func imageFromURL(URL: Foundation.URL?, completionBlock: @escaping (_ downloadedImage: UIImage?)->()) { 16 | guard let URL = URL else { return } 17 | 18 | SDWebImageDownloader.shared().downloadImage(with: URL, options: [SDWebImageDownloaderOptions.useNSURLCache], progress: nil) { 19 | (image, data, error, finished) -> Void in 20 | if let _ = error { NSLog("\(#function): \(error)") } 21 | 22 | DispatchQueue.main.async(execute: { 23 | completionBlock(image) 24 | }) 25 | } 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /PopcornTime/Models/Movie.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Movie.swift 3 | // PopcornTime 4 | // 5 | // Created by Danylo Kostyshyn on 3/19/15. 6 | // Copyright (c) 2015 PopcornTime. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class Movie: BasicInfo { 12 | var videos = [Video]() 13 | 14 | required init(dictionary: [AnyHashable: Any]) { 15 | super.init(dictionary: dictionary) 16 | 17 | let id = (dictionary["id"] as! NSString).intValue 18 | identifier = "\(id)" 19 | title = dictionary["title"] as? String 20 | year = String(describing: dictionary["year"]) 21 | 22 | images = [Image]() 23 | if let cover = dictionary["poster_med"] as? String { 24 | let image = Image(URL: URL(string: cover)!, type: .poster) 25 | images.append(image) 26 | } 27 | 28 | if let cover = dictionary["poster_big"] as? String { 29 | let image = Image(URL: URL(string: cover)!, type: .banner) 30 | images.append(image) 31 | } 32 | 33 | smallImage = self.images.filter({$0.type == ImageType.poster}).first 34 | bigImage = self.images.filter({$0.type == ImageType.banner}).first 35 | synopsis = dictionary["description"] as? String 36 | } 37 | 38 | required init(coder aDecoder: NSCoder) { 39 | super.init(coder: aDecoder)! 40 | } 41 | 42 | override func update(_ dictionary: [AnyHashable: Any]) { 43 | videos.removeAll(keepingCapacity: true) 44 | 45 | let title = dictionary["title"] as! String 46 | 47 | guard let movieList = dictionary["items"] as? [[AnyHashable: Any]] else { return } 48 | for movieDict in movieList { 49 | let quality = movieDict["quality"] as! String 50 | let magnetLink = movieDict["torrent_magnet"] as! String 51 | let size = movieDict["size_bytes"] as! UInt 52 | 53 | let video = Video(name: title, quality: quality, size: size, duration: 0, subGroup: nil, magnetLink: magnetLink) 54 | videos.append(video) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /PopcornTime/Models/PTAPIManager.h: -------------------------------------------------------------------------------- 1 | // 2 | // PTAPIManager.h 3 | // PopcornTime 4 | // 5 | // Created by Danylo Kostyshyn on 2/25/15. 6 | // Copyright (c) 2015 PopcornTime. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | typedef void (^PTAPIManagerFailure)(NSError *error); 12 | typedef void (^PTAPIManagerSuccessItems)(NSArray *items); 13 | typedef void (^PTAPIManagerSuccessItem)(NSDictionary *item); 14 | typedef void (^PTAPIManagerSuccessNone)(); 15 | 16 | typedef NS_ENUM(NSInteger, PTItemType) { 17 | PTItemTypeMovie, 18 | PTItemTypeShow, 19 | PTItemTypeAnime, 20 | }; 21 | 22 | @interface PTAPIManager : NSObject 23 | 24 | + (instancetype)sharedManager; 25 | 26 | - (void)showInfoWithType:(PTItemType)type 27 | withId:(NSString *)imdbId 28 | success:(PTAPIManagerSuccessItem)success 29 | failure:(PTAPIManagerFailure)failure; 30 | 31 | - (void)searchForShowWithType:(PTItemType)type 32 | name:(NSString *)name 33 | success:(PTAPIManagerSuccessItems)success 34 | failure:(PTAPIManagerFailure)failure; 35 | 36 | - (void)topShowsWithType:(PTItemType)type 37 | withPage:(NSUInteger)page 38 | success:(PTAPIManagerSuccessItems)success 39 | failure:(PTAPIManagerFailure)failure; 40 | 41 | // Trakt.tv 42 | /* 43 | + (NSString *)trakttvAccessToken; 44 | + (void)updateTrakttvAccessToken:(NSString *)accessToken; 45 | + (NSURL *)trakttvAuthorizationURL; 46 | - (void)accessTokenWithAuthorizationCode:(NSString *)authorizationCode 47 | success:(void(^)(NSString *accessToken))success 48 | failure:(PTAPIManagerFailure)failure; 49 | 50 | - (void)createListWithName:(NSString *)name 51 | success:(PTAPIManagerSuccessNone)success 52 | failure:(PTAPIManagerFailure)failure; 53 | */ 54 | 55 | @end 56 | -------------------------------------------------------------------------------- /PopcornTime/Models/PTAPIManager.m: -------------------------------------------------------------------------------- 1 | // 2 | // PTAPIManager.m 3 | // PopcornTime 4 | // 5 | // Created by Danylo Kostyshyn on 2/25/15. 6 | // Copyright (c) 2015 PopcornTime. All rights reserved. 7 | // 8 | 9 | #import "PTAPIManager.h" 10 | 11 | #import 12 | 13 | NSUInteger const PTAPIManagerResultsLimit = 30; 14 | NSString *const PTAPIManagerMoviesEndPoint = @"http://api.torrentsapi.com/"; 15 | NSString *const PTAPIManagerShowsEndPoint = @"https://api-fetch.website/tv/"; 16 | NSString *const PTAPIManagerAnimeEndPoint = @"http://ptp.haruhichan.com"; 17 | 18 | @implementation PTAPIManager 19 | 20 | static NSDictionary *YTSHTTPHeaders; 21 | 22 | #pragma mark - Public API 23 | 24 | 25 | + (void)initialize 26 | { 27 | YTSHTTPHeaders = @{@"Host": @"eqwww.image.yt"}; 28 | } 29 | 30 | + (instancetype)sharedManager 31 | { 32 | static dispatch_once_t onceToken; 33 | static PTAPIManager *sharedManager; 34 | dispatch_once(&onceToken, ^{ 35 | sharedManager = [[PTAPIManager alloc] init]; 36 | }); 37 | return sharedManager; 38 | } 39 | 40 | - (void)showInfoWithType:(PTItemType)type 41 | withId:(NSString *)imdbId 42 | success:(PTAPIManagerSuccessItem)success 43 | failure:(PTAPIManagerFailure)failure 44 | { 45 | switch (type) { 46 | case PTItemTypeMovie: { [self movieInfoWithId:imdbId success:success failure:failure]; break; } 47 | case PTItemTypeShow: { [self tvSeriesInfoWithId:imdbId success:success failure:failure]; break; } 48 | case PTItemTypeAnime: { [self animeInfoWithId:imdbId success:success failure:failure]; break; } 49 | default: break; 50 | } 51 | } 52 | 53 | - (void)searchForShowWithType:(PTItemType)type 54 | name:(NSString *)name 55 | success:(PTAPIManagerSuccessItems)success 56 | failure:(PTAPIManagerFailure)failure 57 | { 58 | switch (type) { 59 | case PTItemTypeMovie: { [self searchForMovieWithName:name success:success failure:failure]; break; } 60 | case PTItemTypeShow: { [self searchForTVSeriesWithName:name success:success failure:failure]; break; } 61 | case PTItemTypeAnime: { [self searchForAnimeWithName:name success:success failure:failure]; break; } 62 | default: break; 63 | } 64 | } 65 | 66 | - (void)topShowsWithType:(PTItemType)type 67 | withPage:(NSUInteger)page 68 | success:(PTAPIManagerSuccessItems)success 69 | failure:(PTAPIManagerFailure)failure{ 70 | switch (type) { 71 | case PTItemTypeMovie: { [self topMoviesWithPage:page success:success failure:failure]; break; } 72 | case PTItemTypeShow: { [self topTVSeriesWithPage:page success:success failure:failure]; break; } 73 | case PTItemTypeAnime: { [self topAnimeWithPage:page success:success failure:failure]; break; } 74 | default: break; 75 | } 76 | } 77 | 78 | #pragma mark - Private Methods 79 | 80 | - (void)dataFromURL:(NSURL *)URL 81 | success:(void(^)(id JSONObject))success 82 | failure:(PTAPIManagerFailure)failure 83 | { 84 | return [self dataFromURL:URL HTTPheaders:nil success:success failure:failure]; 85 | } 86 | 87 | - (void)dataFromURL:(NSURL *)URL 88 | HTTPheaders:(NSDictionary *)HTTPheaders 89 | success:(void(^)(id JSONObject))success 90 | failure:(PTAPIManagerFailure)failure 91 | { 92 | [UIApplication sharedApplication].networkActivityIndicatorVisible = YES; 93 | 94 | NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL]; 95 | if (HTTPheaders) { 96 | for (NSString *key in HTTPheaders.allKeys) { 97 | [request addValue:HTTPheaders[key] forHTTPHeaderField:key]; 98 | } 99 | } 100 | 101 | [[[NSURLSession sharedSession] dataTaskWithRequest:request 102 | completionHandler: 103 | ^(NSData *data, NSURLResponse *response, NSError *error) { 104 | dispatch_async(dispatch_get_main_queue(), ^{ 105 | 106 | [UIApplication sharedApplication].networkActivityIndicatorVisible = NO; 107 | 108 | void (^handleError)(NSError *) = ^(NSError *error) { 109 | if (error) { NSLog(@"%@", error); if (failure) { failure(error); } } 110 | }; 111 | 112 | if (error) { handleError(error); return; } 113 | 114 | NSError *JSONError; 115 | id JSONObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:&JSONError]; 116 | if (JSONError) { handleError(JSONError); return; } 117 | 118 | if (success) { success(JSONObject); }; 119 | }); 120 | }] resume]; 121 | } 122 | 123 | #pragma mark Movies 124 | 125 | - (void)topMoviesWithPage:(NSUInteger)page 126 | success:(PTAPIManagerSuccessItems)success 127 | failure:(PTAPIManagerFailure)failure 128 | { 129 | NSString *path = [NSString stringWithFormat:@"list?" 130 | "page=%ld&limit=%ld&order_by=desc&sort_by=seeds&with_rt_ratings=true", (long)page + 1, (long)PTAPIManagerResultsLimit]; 131 | 132 | NSString *URLString = [PTAPIManagerMoviesEndPoint stringByAppendingPathComponent:path]; 133 | [self dataFromURL:[NSURL URLWithString:URLString] success:^(id JSONObject) { 134 | if (success) { 135 | NSArray *items = [((NSDictionary *)JSONObject) objectForKey:@"MovieList"]; 136 | success(items); 137 | 138 | } 139 | } failure:failure]; 140 | } 141 | - (void)movieInfoWithId:(NSString *)imdbId 142 | success:(PTAPIManagerSuccessItem)success 143 | failure:(PTAPIManagerFailure)failure 144 | { 145 | NSString *path = [NSString stringWithFormat:@"list?" 146 | "page=1&limit=5&order_by=desc&sort_by=seeds&with_rt_ratings=true"]; 147 | 148 | // NSString *path = [NSString stringWithFormat:@"list?id=%@", imdbId]; 149 | NSString *URLString = [PTAPIManagerMoviesEndPoint stringByAppendingPathComponent:path]; 150 | [self dataFromURL:[NSURL URLWithString:URLString] success:^(id JSONObject) { 151 | if (success) { 152 | NSDictionary *items = [((NSDictionary *)JSONObject) objectForKey:@"MovieList"]; 153 | for(id key in items) { 154 | NSString *id = [key objectForKey:@"id"]; 155 | if([id isEqualToString:imdbId]) { 156 | success(key); 157 | break; 158 | } 159 | } 160 | // success(items); 161 | } 162 | } failure:failure]; 163 | } 164 | 165 | - (void)searchForMovieWithName:(NSString *)name 166 | success:(PTAPIManagerSuccessItems)success 167 | failure:(PTAPIManagerFailure)failure 168 | { 169 | NSString *path = [[NSString stringWithFormat:@"list?sort_by=seeds&limit=%ld&with_rt_ratings=true&page=1&keywords=%@", (long)PTAPIManagerResultsLimit, name] 170 | stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; 171 | NSString *URLString = [PTAPIManagerMoviesEndPoint stringByAppendingPathComponent:path]; 172 | [self dataFromURL:[NSURL URLWithString:URLString] success:^(id JSONObject) { 173 | if (success) { 174 | NSArray *items = [((NSDictionary *)JSONObject) objectForKey:@"MovieList"]; 175 | success(items); 176 | } 177 | } failure:failure]; 178 | } 179 | 180 | #pragma mark TVSeries 181 | 182 | - (void)topTVSeriesWithPage:(NSUInteger)page 183 | success:(PTAPIManagerSuccessItems)success 184 | failure:(PTAPIManagerFailure)failure 185 | { 186 | NSString *path = [NSString stringWithFormat:@"shows/%ld?limit=%lu", (long)page + 1, (long)PTAPIManagerResultsLimit]; 187 | NSString *URLString = [PTAPIManagerShowsEndPoint stringByAppendingPathComponent:path]; 188 | [self dataFromURL:[NSURL URLWithString:URLString] success:^(id JSONObject) { 189 | if (success) { 190 | success((NSArray *)JSONObject); 191 | } 192 | } failure:failure]; 193 | } 194 | 195 | - (void)tvSeriesInfoWithId:(NSString *)imdbId 196 | success:(PTAPIManagerSuccessItem)success 197 | failure:(PTAPIManagerFailure)failure 198 | { 199 | NSString *path = [NSString stringWithFormat:@"show/%@", imdbId]; 200 | NSString *URLString = [PTAPIManagerShowsEndPoint stringByAppendingPathComponent:path]; 201 | [self dataFromURL:[NSURL URLWithString:URLString] success:^(id JSONObject) { 202 | if (success) { 203 | success((NSDictionary *)JSONObject); 204 | } 205 | } failure:failure]; 206 | } 207 | 208 | - (void)searchForTVSeriesWithName:(NSString *)name 209 | success:(PTAPIManagerSuccessItems)success 210 | failure:(PTAPIManagerFailure)failure 211 | { 212 | NSString *path = [[NSString stringWithFormat:@"shows/1?limit=%ld&keywords=%@&sort=seeds", (long)PTAPIManagerResultsLimit, name] 213 | stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; 214 | NSString *URLString = [PTAPIManagerShowsEndPoint stringByAppendingPathComponent:path]; 215 | [self dataFromURL:[NSURL URLWithString:URLString] success:^(id JSONObject) { 216 | if (success) { 217 | success((NSArray *)JSONObject); 218 | } 219 | } failure:failure]; 220 | } 221 | 222 | #pragma mark Anime 223 | 224 | - (void)topAnimeWithPage:(NSUInteger)page 225 | success:(PTAPIManagerSuccessItems)success 226 | failure:(PTAPIManagerFailure)failure 227 | { 228 | NSString *path = [[NSString stringWithFormat:@"list.php?page=%ld&limit=%ld&sort=popularity&type=All", (long)page, (long)PTAPIManagerResultsLimit] 229 | stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; 230 | NSString *URLString = [PTAPIManagerAnimeEndPoint stringByAppendingPathComponent:path]; 231 | 232 | [self dataFromURL:[NSURL URLWithString:URLString] success:^(id JSONObject) { 233 | if (success) { 234 | success((NSArray *)JSONObject); 235 | } 236 | } failure:failure]; 237 | } 238 | 239 | - (void)animeInfoWithId:(NSString *)imdbId 240 | success:(PTAPIManagerSuccessItem)success 241 | failure:(PTAPIManagerFailure)failure 242 | { 243 | NSString *path = [NSString stringWithFormat:@"anime.php?id=%@", imdbId]; 244 | NSString *URLString = [PTAPIManagerAnimeEndPoint stringByAppendingPathComponent:path]; 245 | [self dataFromURL:[NSURL URLWithString:URLString] success:^(id JSONObject) { 246 | if (success) { 247 | success((NSDictionary *)JSONObject); 248 | } 249 | } failure:failure]; 250 | } 251 | 252 | - (void)searchForAnimeWithName:(NSString *)name 253 | success:(PTAPIManagerSuccessItems)success 254 | failure:(PTAPIManagerFailure)failure { 255 | 256 | NSString *path = [[NSString stringWithFormat:@"/list.php?search=%@&limit=%ld&sort=popularity&type=All", 257 | [name stringByReplacingOccurrencesOfString:@" " withString:@"+"], (long)PTAPIManagerResultsLimit] 258 | stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; 259 | NSString *URLString = [PTAPIManagerAnimeEndPoint stringByAppendingPathComponent:path]; 260 | 261 | [self dataFromURL:[NSURL URLWithString:URLString] success:^(id JSONObject) { 262 | if (success) { 263 | success((NSArray *)JSONObject); 264 | } 265 | } failure:failure]; 266 | } 267 | 268 | #pragma mark - Trakt.tv 269 | 270 | /* 271 | 272 | NSString *const PTAPIManagerTrakttvAccessTokenKey = @"TrakttvAccessToken"; 273 | 274 | NSString *const PTAPIManagerTrakttvAPIEndPoint = @"http://api.trakt.tv"; 275 | NSString *const PTAPIManagerTrakttvAPIKey = @"df8d400233727be104e5caf40e07d785b6963c0e194dcbd24f806e8a4e243167"; 276 | NSString *const PTAPIManagerTrakttvAPIVersion = @"2"; 277 | 278 | NSString *const PTAPIManagerTrakttvClientId = @"df8d400233727be104e5caf40e07d785b6963c0e194dcbd24f806e8a4e243167"; 279 | NSString *const PTAPIManagerTrakttvClientSecret = @"1a98885c5271f7162ac51b2c1dd09decc55df127f5e1b29af533d35eee5df9b2"; 280 | NSString *const PTAPIManagerTrakttvRedirectURL = @"urn:ietf:wg:oauth:2.0:oob"; 281 | 282 | + (NSString *)trakttvAccessToken 283 | { 284 | return [[NSUserDefaults standardUserDefaults] objectForKey:PTAPIManagerTrakttvAccessTokenKey]; 285 | } 286 | 287 | + (void)updateTrakttvAccessToken:(NSString *)accessToken 288 | { 289 | [[NSUserDefaults standardUserDefaults] setObject:accessToken forKey:PTAPIManagerTrakttvAccessTokenKey]; 290 | [[NSUserDefaults standardUserDefaults] synchronize]; 291 | } 292 | 293 | + (NSURL *)trakttvAuthorizationURL 294 | { 295 | NSString *authPath = [NSString stringWithFormat:@"http://trakt.tv/oauth/authorize?client_id=%@&redirect_uri=%@&response_type=code", 296 | PTAPIManagerTrakttvClientId, 297 | PTAPIManagerTrakttvRedirectURL]; 298 | return [NSURL URLWithString:authPath]; 299 | } 300 | 301 | - (void)trakttvPerformRequestWithAPIMethod:(NSString *)APIMethod 302 | HTTPMethod:(NSString *)HTTPMethod 303 | HTTPBodyPayload:(NSDictionary *)HTTPBodyPayload 304 | OAuthRequired:(BOOL)OAuthRequired 305 | success:(void(^)(id JSONObject))success 306 | failure:(PTAPIManagerFailure)failure 307 | { 308 | NSString *path = [PTAPIManagerTrakttvAPIEndPoint stringByAppendingPathComponent:APIMethod]; 309 | NSURL *URL = [NSURL URLWithString:path]; 310 | NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL]; 311 | request.HTTPMethod = HTTPMethod; 312 | 313 | // Configure headers 314 | [request addValue:@"application/json" forHTTPHeaderField:@"Content-type"]; 315 | [request addValue:PTAPIManagerTrakttvAPIKey forHTTPHeaderField:@"trakt-api-key"]; 316 | [request addValue:PTAPIManagerTrakttvAPIVersion forHTTPHeaderField:@"trakt-api-version"]; 317 | 318 | if (OAuthRequired) { 319 | NSString *bearerToken = [NSString stringWithFormat:@"Bearer [%@]", [PTAPIManager trakttvAccessToken]]; 320 | [request addValue:bearerToken forHTTPHeaderField:@"Authorization"]; 321 | } 322 | 323 | void (^handleError)(NSError *) = ^(NSError *error) { 324 | if (error) { NSLog(@"%@", error); if (failure) { failure(error); } } 325 | }; 326 | 327 | // Configure body 328 | NSError *JSONError; 329 | NSData *JSONBody = [NSJSONSerialization dataWithJSONObject:HTTPBodyPayload options:0 error:&JSONError]; 330 | handleError(JSONError); 331 | request.HTTPBody = JSONBody; 332 | 333 | [[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler: 334 | ^(NSData *data, NSURLResponse *response, NSError *error) { 335 | dispatch_async(dispatch_get_main_queue(), ^{ 336 | if (error) { handleError(error); return; } 337 | 338 | NSError *JSONError; 339 | id JSONObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:&JSONError]; 340 | if (JSONError) { handleError(JSONError); return; } 341 | 342 | if (success) { success(JSONObject); } 343 | }); 344 | }] resume]; 345 | } 346 | 347 | - (void)accessTokenWithAuthorizationCode:(NSString *)authorizationCode 348 | success:(void(^)(NSString *accessToken))success 349 | failure:(PTAPIManagerFailure)failure 350 | { 351 | NSString *APIMethod = @"oauth/token"; 352 | NSDictionary *HTTPBodyPayload = @{@"code": authorizationCode, 353 | @"client_id": PTAPIManagerTrakttvClientId, 354 | @"client_secret": PTAPIManagerTrakttvClientSecret, 355 | @"redirect_uri": PTAPIManagerTrakttvRedirectURL, 356 | @"grant_type": @"authorization_code"}; 357 | 358 | [self trakttvPerformRequestWithAPIMethod:APIMethod 359 | HTTPMethod:@"POST" 360 | HTTPBodyPayload:HTTPBodyPayload 361 | OAuthRequired:NO 362 | success:^(id JSONObject) { 363 | if (success) { 364 | success([((NSDictionary *)JSONObject) objectForKey:@"access_token"]); 365 | } 366 | } 367 | failure:failure]; 368 | } 369 | 370 | - (void)createListWithName:(NSString *)name 371 | success:(PTAPIManagerSuccessNone)success 372 | failure:(PTAPIManagerFailure)failure 373 | { 374 | NSString *APIMethod = @"users/me/lists"; 375 | NSDictionary *HTTPBodyPayload = @{@"name": @"ololo", 376 | @"description": @"ololo", 377 | @"privacy": @"private", 378 | @"display_numbers": @"false", 379 | @"allow_comments": @"true"}; 380 | 381 | [self trakttvPerformRequestWithAPIMethod:APIMethod 382 | HTTPMethod:@"POST" 383 | HTTPBodyPayload:HTTPBodyPayload 384 | OAuthRequired:YES 385 | success:nil failure:failure]; 386 | } 387 | 388 | */ 389 | 390 | @end 391 | -------------------------------------------------------------------------------- /PopcornTime/Models/PTTorrentStreamer.h: -------------------------------------------------------------------------------- 1 | // 2 | // PTTorrentStreamer.h 3 | // PopcornTime 4 | // 5 | // Created by Danylo Kostyshyn on 2/23/15. 6 | // Copyright (c) 2015 PopcornTime. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | typedef struct { 12 | float bufferingProgress; 13 | float totalProgreess; 14 | int downloadSpeed; 15 | int upoadSpeed; 16 | int seeds; 17 | int peers; 18 | } PTTorrentStatus; 19 | 20 | typedef void (^PTTorrentStreamerProgress)(PTTorrentStatus status); 21 | typedef void (^PTTorrentStreamerReadyToPlay)(NSURL *videoFileURL); 22 | typedef void (^PTTorrentStreamerFailure)(NSError *error); 23 | 24 | @interface PTTorrentStreamer : NSObject 25 | 26 | + (instancetype)sharedStreamer; 27 | 28 | - (void)startStreamingFromFileOrMagnetLink:(NSString *)filePathOrMagnetLink 29 | progress:(PTTorrentStreamerProgress)progreess 30 | readyToPlay:(PTTorrentStreamerReadyToPlay)readyToPlay 31 | failure:(PTTorrentStreamerFailure)failure; 32 | 33 | - (void)cancelStreaming; 34 | 35 | @end 36 | -------------------------------------------------------------------------------- /PopcornTime/Models/PTTorrentStreamer.mm: -------------------------------------------------------------------------------- 1 | // 2 | // PTTorrentStreamer.m 3 | // PopcornTime 4 | // 5 | // Created by Danylo Kostyshyn on 2/23/15. 6 | // Copyright (c) 2015 PopcornTime. All rights reserved. 7 | // 8 | 9 | #import "PTTorrentStreamer.h" 10 | 11 | #import 12 | 13 | #import 14 | #import 15 | #import 16 | #import 17 | 18 | #import 19 | 20 | using namespace libtorrent; 21 | 22 | @interface PTTorrentStreamer() 23 | @property (nonatomic, strong) dispatch_queue_t alertsQueue; 24 | @property (nonatomic, getter=isAlertsLoopActive) BOOL alertsLoopActive; 25 | @property (nonatomic, strong) NSString *savePath; 26 | @property (nonatomic, getter=isDownloading) BOOL downloading; 27 | @property (nonatomic, getter=isStreaming) BOOL streaming; 28 | 29 | @property (nonatomic, copy) PTTorrentStreamerProgress progressBlock; 30 | @property (nonatomic, copy) PTTorrentStreamerReadyToPlay readyToPlayBlock; 31 | @property (nonatomic, copy) PTTorrentStreamerFailure failureBlock; 32 | @end 33 | 34 | @implementation PTTorrentStreamer 35 | { 36 | session *_session; 37 | std::vector required_pieces; 38 | } 39 | 40 | + (instancetype)sharedStreamer 41 | { 42 | static dispatch_once_t onceToken; 43 | static PTTorrentStreamer *sharedStreamer; 44 | dispatch_once(&onceToken, ^{ 45 | sharedStreamer = [[PTTorrentStreamer alloc] init]; 46 | }); 47 | return sharedStreamer; 48 | } 49 | 50 | - (instancetype)init 51 | { 52 | self = [super init]; 53 | if (self) { 54 | [self setupSession]; 55 | } 56 | return self; 57 | } 58 | 59 | #pragma mark - 60 | 61 | + (NSString *)downloadsDirectory 62 | { 63 | NSString *downloadsDirectoryPath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"Downloads"]; 64 | if (![[NSFileManager defaultManager] fileExistsAtPath:downloadsDirectoryPath]) { 65 | NSError *error; 66 | [[NSFileManager defaultManager] createDirectoryAtPath:downloadsDirectoryPath 67 | withIntermediateDirectories:YES 68 | attributes:nil 69 | error:&error]; 70 | if (error) { 71 | NSLog(@"%@", error); 72 | return nil; 73 | } 74 | } 75 | return downloadsDirectoryPath; 76 | } 77 | 78 | - (void)setupSession 79 | { 80 | // _session = new session(fingerprint("PopcornTime", 1, 0, 0, 0), 81 | // std::make_pair(6881, 6889), 82 | // 0, 83 | // session::start_default_features | session::add_default_plugins, 84 | // alert::all_categories); 85 | 86 | error_code ec; 87 | 88 | _session = new session(); 89 | _session->set_alert_mask(alert::all_categories); 90 | _session->listen_on(std::make_pair(6881, 6889), ec); 91 | if (ec) { 92 | NSLog(@"failed to open listen socket: %s", ec.message().c_str()); 93 | } 94 | 95 | session_settings settings = _session->settings(); 96 | settings.announce_to_all_tiers = true; 97 | settings.announce_to_all_trackers = true; 98 | settings.prefer_udp_trackers = false; 99 | settings.max_peerlist_size = 0; 100 | _session->set_settings(settings); 101 | } 102 | 103 | - (void)startStreamingFromFileOrMagnetLink:(NSString *)filePathOrMagnetLink 104 | progress:(PTTorrentStreamerProgress)progreess 105 | readyToPlay:(PTTorrentStreamerReadyToPlay)readyToPlay 106 | failure:(PTTorrentStreamerFailure)failure; 107 | { 108 | self.progressBlock = progreess; 109 | self.readyToPlayBlock = readyToPlay; 110 | self.failureBlock = failure; 111 | 112 | self.alertsQueue = dispatch_queue_create("com.popcorntime.ios.torrentstreamer.alerts", DISPATCH_QUEUE_SERIAL); 113 | self.alertsLoopActive = YES; 114 | dispatch_async(self.alertsQueue, ^{ 115 | [self alertsLoop]; 116 | }); 117 | 118 | error_code ec; 119 | add_torrent_params tp; 120 | 121 | NSString *MD5String = nil; 122 | 123 | if ([filePathOrMagnetLink hasPrefix:@"magnet"]) { 124 | NSString *magnetLink = filePathOrMagnetLink; 125 | magnetLink = [magnetLink stringByAppendingString:@"&tr=udp://open.demonii.com:1337" 126 | "&tr=udp://tracker.coppersurfer.tk:6969"]; 127 | tp.url = std::string([magnetLink UTF8String]); 128 | 129 | MD5String = [CocoaSecurity md5:magnetLink].hexLower; 130 | } else { 131 | NSString *filePath = filePathOrMagnetLink; 132 | if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) { 133 | NSData *fileData = [NSData dataWithContentsOfFile:filePath]; 134 | MD5String = [CocoaSecurity md5WithData:fileData].hexLower; 135 | 136 | tp.ti = new torrent_info([filePathOrMagnetLink UTF8String], ec); 137 | if (ec) { 138 | NSLog(@"%s", ec.message().c_str()); 139 | return; 140 | } 141 | } else { 142 | NSLog(@"File doesn't exists at path: %@", filePath); 143 | return; 144 | } 145 | } 146 | 147 | NSString *halfMD5String = [MD5String substringToIndex:16]; 148 | self.savePath = [[PTTorrentStreamer downloadsDirectory] stringByAppendingPathComponent:halfMD5String]; 149 | 150 | NSError *error; 151 | [[NSFileManager defaultManager] createDirectoryAtPath:self.savePath 152 | withIntermediateDirectories:YES 153 | attributes:nil 154 | error:&error]; 155 | if (error) { 156 | NSLog(@"Can't create directory at path: %@", self.savePath); 157 | return; 158 | } 159 | 160 | tp.save_path = std::string([self.savePath UTF8String]); 161 | tp.storage_mode = storage_mode_allocate; 162 | 163 | torrent_handle th = _session->add_torrent(tp, ec); 164 | th.set_sequential_download(true); 165 | 166 | if (ec) { 167 | NSLog(@"%s", ec.message().c_str()); 168 | return; 169 | } 170 | 171 | self.downloading = YES; 172 | [UIApplication sharedApplication].networkActivityIndicatorVisible = YES; 173 | } 174 | 175 | - (void)cancelStreaming 176 | { 177 | if ([self isDownloading]) { 178 | self.alertsQueue = nil; 179 | self.alertsLoopActive = NO; 180 | 181 | std::vector ths = _session->get_torrents(); 182 | for(std::vector::size_type i = 0; i != ths.size(); i++) { 183 | _session->remove_torrent(ths[i], session::delete_files); 184 | } 185 | 186 | required_pieces.clear(); 187 | 188 | self.progressBlock = nil; 189 | self.readyToPlayBlock = nil; 190 | self.failureBlock = nil; 191 | 192 | NSError *error; 193 | [[NSFileManager defaultManager] removeItemAtPath:self.savePath error:&error]; 194 | if (error) NSLog(@"%@", error); 195 | 196 | self.savePath = nil; 197 | 198 | self.streaming = NO; 199 | self.downloading = NO; 200 | [UIApplication sharedApplication].networkActivityIndicatorVisible = NO; 201 | } 202 | } 203 | 204 | #pragma mark - Alerts Loop 205 | 206 | #define ALERTS_LOOP_WAIT_MILLIS 500 207 | #define MIN_PIECES 15 208 | #define PIECE_DEADLINE_MILLIS 100 209 | #define LIBTORRENT_PRIORITY_SKIP 0 210 | #define LIBTORRENT_PRIORITY_MAXIMUM 7 211 | 212 | - (void)alertsLoop 213 | { 214 | std::deque deque; 215 | time_duration max_wait = milliseconds(ALERTS_LOOP_WAIT_MILLIS); 216 | 217 | while ([self isAlertsLoopActive]) 218 | { 219 | const alert *ptr = _session->wait_for_alert(max_wait); 220 | if (ptr != nullptr) { 221 | _session->pop_alerts(&deque); 222 | for (std::deque::iterator it=deque.begin(); it != deque.end(); ++it) { 223 | std::unique_ptr alert(*it); 224 | // NSLog(@"type:%d msg:%s", alert->type(), alert->message().c_str()); 225 | switch (alert->type()) { 226 | case metadata_received_alert::alert_type: 227 | [self metadataReceivedAlert:(metadata_received_alert *)alert.get()]; 228 | break; 229 | case block_finished_alert::alert_type: 230 | [self pieceFinishedAlert:(piece_finished_alert *)alert.get()]; 231 | break; 232 | // In case the video file is already fully downloaded 233 | case torrent_finished_alert::alert_type: 234 | [self torrentFinishedAlert:(torrent_finished_alert *)alert.get()]; 235 | break; 236 | default: break; 237 | } 238 | } 239 | deque.clear(); 240 | } 241 | } 242 | } 243 | 244 | - (void)prioritizeNextPieces:(torrent_handle)th 245 | { 246 | int next_required_piece = required_pieces[MIN_PIECES-1]+1; 247 | required_pieces.clear(); 248 | 249 | boost::intrusive_ptr ti = th.torrent_file(); 250 | 251 | for (int i=next_required_piece; inum_pieces()) { 253 | th.piece_priority(i, LIBTORRENT_PRIORITY_MAXIMUM); 254 | th.set_piece_deadline(i, PIECE_DEADLINE_MILLIS, torrent_handle::alert_when_available); 255 | required_pieces.push_back(i); 256 | } 257 | } 258 | } 259 | 260 | - (void)processTorrent:(torrent_handle)th 261 | { 262 | if (![self isStreaming]) { 263 | self.streaming = YES; 264 | if (self.readyToPlayBlock) { 265 | boost::intrusive_ptr ti = th.torrent_file(); 266 | int file_index = [self indexOfLargestFileInTorrent:th]; 267 | file_entry fe = ti->file_at(file_index); 268 | std::string path = fe.path; 269 | 270 | NSString *fileName = [NSString stringWithCString:path.c_str() encoding:NSUTF8StringEncoding]; 271 | NSURL *fileURL = [NSURL fileURLWithPath:[self.savePath stringByAppendingPathComponent:fileName]]; 272 | 273 | dispatch_async(dispatch_get_main_queue(), ^{ 274 | self.readyToPlayBlock(fileURL); 275 | }); 276 | } 277 | } 278 | } 279 | 280 | - (int)indexOfLargestFileInTorrent:(torrent_handle)th 281 | { 282 | boost::intrusive_ptr ti = th.torrent_file(); 283 | int files_count = ti->num_files(); 284 | if (files_count > 1) { 285 | size_type largest_size = -1; 286 | int largest_file_index = -1; 287 | for (int i=0; ifile_at(i); 289 | if (fe.size > largest_size) { 290 | largest_size = fe.size; 291 | largest_file_index = i; 292 | } 293 | } 294 | return largest_file_index; 295 | } 296 | return 0; 297 | } 298 | 299 | #pragma mark - Logging 300 | 301 | - (void)logPiecesStatus:(torrent_handle)th 302 | { 303 | NSString *pieceStatus = @""; 304 | boost::intrusive_ptr ti = th.torrent_file(); 305 | for(std::vector::size_type i=0; i!=required_pieces.size(); i++) { 306 | int piece = required_pieces[i]; 307 | pieceStatus = [pieceStatus stringByAppendingFormat:@"%d:%d ", piece, th.have_piece(piece)]; 308 | } 309 | NSLog(@"%@", pieceStatus); 310 | } 311 | 312 | - (void)logTorrentStatus:(PTTorrentStatus)status 313 | { 314 | NSString *speedString = [NSByteCountFormatter stringFromByteCount:status.downloadSpeed 315 | countStyle:NSByteCountFormatterCountStyleBinary]; 316 | NSLog(@"%.0f%%, %.0f%%, %@/s, %d, %d", 317 | status.bufferingProgress*100, status.totalProgreess*100, 318 | speedString, status.seeds, status.peers); 319 | } 320 | 321 | #pragma mark - Alerts 322 | 323 | - (void)metadataReceivedAlert:(metadata_received_alert *)alert 324 | { 325 | torrent_handle th = alert->handle; 326 | int file_index = [self indexOfLargestFileInTorrent:th]; 327 | 328 | std::vector file_priorities = th.file_priorities(); 329 | std::fill(file_priorities.begin(), file_priorities.end(), LIBTORRENT_PRIORITY_SKIP); 330 | file_priorities[file_index] = LIBTORRENT_PRIORITY_MAXIMUM; 331 | th.prioritize_files(file_priorities); 332 | 333 | boost::intrusive_ptr ti = th.torrent_file(); 334 | int first_piece = ti->map_file(file_index, 0, 0).piece; 335 | for (int i=first_piece; ifile_at(file_index).size; 340 | int last_piece = ti->map_file(file_index, file_size-1, 0).piece; 341 | required_pieces.push_back(last_piece); 342 | 343 | for (int i=1; i<10; i++) { 344 | required_pieces.push_back(last_piece-i); 345 | } 346 | 347 | for(std::vector::size_type i=0; i!=required_pieces.size(); i++) { 348 | int piece = required_pieces[i]; 349 | th.piece_priority(piece, LIBTORRENT_PRIORITY_MAXIMUM); 350 | th.set_piece_deadline(piece, PIECE_DEADLINE_MILLIS, torrent_handle::alert_when_available); 351 | } 352 | } 353 | 354 | - (void)pieceFinishedAlert:(piece_finished_alert *)alert 355 | { 356 | torrent_handle th = alert->handle; 357 | torrent_status status = th.status(); 358 | 359 | int requiredPiecesDownloaded = 0; 360 | BOOL allRequiredPiecesDownloaded = YES; 361 | for(std::vector::size_type i=0; i!=required_pieces.size(); i++) { 362 | int piece = required_pieces[i]; 363 | if (th.have_piece(piece)) { 364 | requiredPiecesDownloaded++; 365 | } else { 366 | allRequiredPiecesDownloaded = NO; 367 | } 368 | } 369 | 370 | [self logPiecesStatus:th]; 371 | 372 | int requiredPieces = (int)required_pieces.size(); 373 | float bufferingProgress = 1.0 - (requiredPieces-requiredPiecesDownloaded)/(float)requiredPieces; 374 | 375 | PTTorrentStatus torrentStatus = {bufferingProgress, 376 | status.progress, 377 | status.download_rate, 378 | status.upload_rate, 379 | status.num_seeds, 380 | status.num_peers}; 381 | [self logTorrentStatus:torrentStatus]; 382 | 383 | if (self.progressBlock) { 384 | dispatch_async(dispatch_get_main_queue(), ^{ 385 | self.progressBlock(torrentStatus); 386 | }); 387 | } 388 | 389 | if (allRequiredPiecesDownloaded) { 390 | [self prioritizeNextPieces:th]; 391 | [self processTorrent:th]; 392 | } 393 | } 394 | 395 | - (void)torrentFinishedAlert:(torrent_finished_alert *)alert 396 | { 397 | [self processTorrent:alert->handle]; 398 | } 399 | 400 | @end 401 | -------------------------------------------------------------------------------- /PopcornTime/Models/ParseManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ParseManager.swift 3 | // 4 | // 5 | // Created by Andriy K. on 6/23/15. 6 | // 7 | // 8 | 9 | import UIKit 10 | 11 | class ParseShowData: NSObject { 12 | 13 | private var collection = [String : PFObject]() 14 | 15 | convenience init(episodesFromParse: [PFObject]) { 16 | self.init() 17 | for episode in episodesFromParse { 18 | let seasonIndex = episode.objectForKey(ParseManager.sharedInstance.seasonKey) as! Int 19 | let episodeIndex = episode.objectForKey(ParseManager.sharedInstance.episodeKey) as! Int 20 | let key = dictKey(seasonIndex, episode: episodeIndex) 21 | collection[key] = episode 22 | } 23 | } 24 | 25 | func isEpisodeWatched(season: Int, episode: Int) -> Bool { 26 | let key = dictKey(season, episode: episode) 27 | if let episode = collection[key] { 28 | if let isWatched = episode.objectForKey(ParseManager.sharedInstance.watchedKey) as? Bool { 29 | return isWatched 30 | } 31 | } 32 | return false 33 | } 34 | 35 | private func dictKey(season: Int, episode: Int) -> String { 36 | return "\(season)_\(episode)" 37 | } 38 | 39 | } 40 | 41 | class ParseManager: NSObject { 42 | 43 | static let sharedInstance = ParseManager() 44 | 45 | let showClassName = "Show" 46 | let episodeClassName = "Episode" 47 | let showIdKey = "showId" 48 | let userKey = "user" 49 | let titleKey = "title" 50 | let seasonKey = "season" 51 | let episodeKey = "episodeNumber" 52 | let showKey = "show" 53 | let watchedKey = "watched" 54 | 55 | private override init() { 56 | } 57 | 58 | // MARK: - Public API 59 | 60 | var user: PFUser? { 61 | return PFUser.currentUser() 62 | } 63 | 64 | func markEpisode(episodeInfo: Episode, basicInfo: BasicInfo) { 65 | if let user = user { 66 | let query = PFQuery(className:showClassName) 67 | query.whereKey(userKey, equalTo:user) 68 | query.whereKey(showIdKey, equalTo:basicInfo.identifier) 69 | query.findObjectsInBackgroundWithBlock { (objects, error) -> Void in 70 | 71 | var show: PFObject 72 | 73 | if let object = objects?.first as PFObject? { 74 | show = object 75 | } else { 76 | show = PFObject(className: self.showClassName) 77 | show.setObject(basicInfo.identifier, forKey: self.showIdKey) 78 | if let title = basicInfo.title { 79 | show.setObject(title, forKey: self.titleKey) 80 | } 81 | let relation = show.relationForKey(self.userKey) 82 | relation.addObject(user) 83 | } 84 | 85 | show.saveInBackgroundWithBlock({ (success, error) -> Void in 86 | 87 | let queryEpisode = PFQuery(className:self.episodeClassName) 88 | queryEpisode.whereKey(self.seasonKey, equalTo:episodeInfo.seasonNumber) 89 | queryEpisode.whereKey(self.episodeKey, equalTo:episodeInfo.episodeNumber) 90 | queryEpisode.whereKey(self.showKey, equalTo: show) 91 | 92 | queryEpisode.findObjectsInBackgroundWithBlock { (episodes, episodeError) -> Void in 93 | 94 | var episode: PFObject 95 | 96 | if let ep = episodes?.first as PFObject? { 97 | episode = ep 98 | } else { 99 | let newEpisode = PFObject(className: self.episodeClassName) 100 | let relationShow = newEpisode.relationForKey(self.showKey) 101 | relationShow.addObject(show) 102 | newEpisode.setObject(episodeInfo.seasonNumber, forKey: self.seasonKey) 103 | newEpisode.setObject(episodeInfo.episodeNumber, forKey: self.episodeKey) 104 | episode = newEpisode 105 | } 106 | episode.setObject(true, forKey: self.watchedKey) 107 | 108 | episode.saveInBackgroundWithBlock(nil) 109 | } 110 | }) 111 | } 112 | } 113 | } 114 | 115 | /// Mark [Episode] as watched on Parse. 116 | func markEpisodes(episodesInfo: [Episode], basicInfo: BasicInfo, completionHandler: PFBooleanResultBlock?) { 117 | 118 | if episodesInfo.count == 0 { 119 | return 120 | } 121 | 122 | let episodeNumbers = episodesInfo.map(){ episode in 123 | return episode.episodeNumber 124 | } 125 | let seasonNumber = episodesInfo.first!.seasonNumber 126 | 127 | 128 | if let user = user { 129 | let query = PFQuery(className:showClassName) 130 | query.whereKey(userKey, equalTo:user) 131 | query.whereKey(showIdKey, equalTo:basicInfo.identifier) 132 | query.findObjectsInBackgroundWithBlock { 133 | (objects, error) -> Void in 134 | 135 | var show: PFObject 136 | 137 | if let object = objects?.first as PFObject? { 138 | show = object 139 | } else { 140 | show = PFObject(className: self.showClassName) 141 | show.setObject(basicInfo.identifier, forKey: self.showIdKey) 142 | if let title = basicInfo.title { 143 | show.setObject(title, forKey: self.titleKey) 144 | } 145 | let relation = show.relationForKey(self.userKey) 146 | relation.addObject(user) 147 | } 148 | 149 | show.saveInBackgroundWithBlock({ (success, error) -> Void in 150 | let queryEpisode = PFQuery(className:self.episodeClassName) 151 | queryEpisode.whereKey(self.seasonKey, equalTo: seasonNumber) 152 | queryEpisode.whereKey(self.episodeKey, containedIn: episodeNumbers) 153 | queryEpisode.whereKey(self.showKey, equalTo: show) 154 | 155 | queryEpisode.findObjectsInBackgroundWithBlock { (results, episodeError) -> Void in 156 | 157 | var marked = [UInt]() 158 | var pfObjects = [PFObject]() 159 | 160 | if let parseEpisodes = results as [PFObject]? { 161 | print("\(parseEpisodes.count): episodes already on Parse") 162 | for parseEp in parseEpisodes { 163 | parseEp.setObject(true, forKey: self.watchedKey) 164 | if let parseEpNumber = parseEp.objectForKey(self.episodeKey) as? UInt { 165 | marked.append(parseEpNumber) 166 | pfObjects.append(parseEp) 167 | print("parse ep:\(parseEpNumber) marked") 168 | } 169 | } 170 | } 171 | 172 | 173 | for ep in episodesInfo { 174 | if marked.contains(ep.episodeNumber) == false { 175 | let newEpisode = PFObject(className: self.episodeClassName) 176 | let relationShow = newEpisode.relationForKey(self.showKey) 177 | relationShow.addObject(show) 178 | newEpisode.setObject(ep.seasonNumber, forKey: self.seasonKey) 179 | newEpisode.setObject(ep.episodeNumber, forKey: self.episodeKey) 180 | newEpisode.setObject(true, forKey: self.watchedKey) 181 | marked.append(ep.episodeNumber) 182 | pfObjects.append(newEpisode) 183 | print("ep:\(ep.episodeNumber) marked") 184 | } 185 | } 186 | 187 | PFObject.saveAllInBackground(pfObjects, block: completionHandler) 188 | } 189 | }) 190 | } 191 | } 192 | } 193 | 194 | func parseEpisodesData(basicInfo: BasicInfo, handler: (ParseShowData) -> Void) { 195 | if let user = user { 196 | let query = PFQuery(className: showClassName) 197 | query.whereKey(userKey, equalTo: user) 198 | query.whereKey(showIdKey, equalTo:basicInfo.identifier) 199 | query.findObjectsInBackgroundWithBlock({ (results, error) -> Void in 200 | if let show = results?.first as PFObject? { 201 | let queryEpisode = PFQuery(className: self.episodeClassName) 202 | queryEpisode.whereKey(self.showKey, equalTo: show) 203 | do { 204 | let episodes = try queryEpisode.findObjects() as [PFObject] 205 | let parserData = ParseShowData(episodesFromParse: episodes) 206 | handler(parserData) 207 | } catch let error as NSError { 208 | print(error) 209 | } 210 | } 211 | }) 212 | } 213 | } 214 | 215 | } 216 | -------------------------------------------------------------------------------- /PopcornTime/Models/Show.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Show.swift 3 | // PopcornTime 4 | // 5 | // Created by Danylo Kostyshyn on 3/19/15. 6 | // Copyright (c) 2015 PopcornTime. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class Show: BasicInfo { 12 | var seasons = [Season]() 13 | 14 | func thumbnail(_ original: String) -> String { 15 | return original.replacingOccurrences(of: "original", with: "thumb", 16 | options: NSString.CompareOptions.caseInsensitive, range: nil) 17 | } 18 | 19 | required init(dictionary: [AnyHashable: Any]) { 20 | super.init(dictionary: dictionary) 21 | 22 | identifier = dictionary["imdb_id"] as! String 23 | title = dictionary["title"] as? String 24 | year = dictionary["year"] as? String 25 | 26 | if let imagesDict = dictionary["images"] as? NSDictionary { 27 | images = [Image]() 28 | if let banner = imagesDict["banner"] as? String { 29 | let URL = Foundation.URL(string: thumbnail(banner)) 30 | let image = Image(URL: URL!, type: .banner) 31 | images.append(image) 32 | } 33 | if let fanart = imagesDict["fanart"] as? String { 34 | let URL = Foundation.URL(string: thumbnail(fanart)) 35 | let image = Image(URL: URL!, type: .fanart) 36 | images.append(image) 37 | } 38 | if let poster = imagesDict["poster"] as? String { 39 | let URL = Foundation.URL(string: thumbnail(poster)) 40 | let image = Image(URL: URL!, type: .poster) 41 | images.append(image) 42 | } 43 | 44 | smallImage = images.filter({$0.type == ImageType.poster}).first 45 | bigImage = images.filter({$0.type == ImageType.fanart}).first 46 | } 47 | } 48 | 49 | required init(coder aDecoder: NSCoder) { 50 | super.init(coder: aDecoder)! 51 | } 52 | 53 | override func update(_ dictionary: [AnyHashable: Any]) { 54 | synopsis = dictionary["synopsis"] as? String 55 | 56 | seasons.removeAll(keepingCapacity: true) 57 | var allEpisodes = [Episode]() 58 | var allSeasonsNumbers = [UInt:Bool]() 59 | 60 | guard let episodesDicts = dictionary["episodes"] as? [[AnyHashable: Any]] else { return } 61 | for episodeDict in episodesDicts { 62 | 63 | var videos = [Video]() 64 | 65 | if let torrents = episodeDict["torrents"] as? [String : NSDictionary] { 66 | for torrent in torrents { 67 | let quality = torrent.0 68 | if quality == "0" { 69 | continue 70 | } 71 | 72 | let url = torrent.1["url"] as! String 73 | 74 | let video = Video(name: nil, quality: quality, size: 0, duration: 0, subGroup: nil, magnetLink: url) 75 | videos.append(video) 76 | } 77 | } 78 | 79 | videos = videos.sorted(by: { (a, b) -> Bool in 80 | var aQuality: Int = 0 81 | Scanner(string: a.quality!).scanInt(&aQuality) 82 | 83 | var bQuality: Int = 0 84 | Scanner(string: b.quality!).scanInt(&bQuality) 85 | 86 | return aQuality < bQuality 87 | }) 88 | 89 | let seasonNumber = (episodeDict["season"] as! UInt) 90 | if (allSeasonsNumbers[seasonNumber] == nil) { 91 | allSeasonsNumbers[seasonNumber] = true 92 | } 93 | 94 | let title = episodeDict["title"] as? String 95 | let episodeNumber = (episodeDict["episode"] as! UInt) 96 | 97 | let synopsis = episodeDict["overview"] as? String 98 | let episode = Episode(title: title, desc: synopsis, seasonNumber: seasonNumber, episodeNumber: episodeNumber, videos: videos) 99 | allEpisodes.append(episode) 100 | } 101 | 102 | var seasonsNumbers = Array(allSeasonsNumbers.keys) 103 | seasonsNumbers.sort(by: { (a, b) -> Bool in 104 | return a < b 105 | }) 106 | 107 | for seasonNumber in seasonsNumbers { 108 | let seasonEpisodes = allEpisodes.filter({ (episode) -> Bool in 109 | return episode.seasonNumber == seasonNumber 110 | }) 111 | 112 | if seasonEpisodes.count > 0{ 113 | let season = Season(seasonNumber: seasonNumber, episodes: seasonEpisodes) 114 | seasons.append(season) 115 | } 116 | } 117 | } 118 | } 119 | 120 | extension Show: ContainsEpisodes { 121 | func episodeFor(seasonIndex: Int, episodeIndex: Int) -> Episode { 122 | let episode = seasons[seasonIndex].episodes[episodeIndex] 123 | return episode 124 | } 125 | 126 | func episodesFor(seasonIndex: Int) -> [Episode] { 127 | return seasons[seasonIndex].episodes 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /PopcornTime/PopcornTime-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | #import "PTAPIManager.h" 6 | #import "PTTorrentStreamer.h" 7 | #import "VDLPlaybackViewController.h" 8 | #import 9 | #import 10 | #import 11 | -------------------------------------------------------------------------------- /PopcornTime/Resources/Images.xcassets/AnimeIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x", 10 | "filename" : "anime.png" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /PopcornTime/Resources/Images.xcassets/AnimeIcon.imageset/anime.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danylokos/popcorntime-ios/555aff8d10e38870ef58e75c1af35e5494547c98/PopcornTime/Resources/Images.xcassets/AnimeIcon.imageset/anime.png -------------------------------------------------------------------------------- /PopcornTime/Resources/Images.xcassets/AppIcon.appiconset/AppIcon60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danylokos/popcorntime-ios/555aff8d10e38870ef58e75c1af35e5494547c98/PopcornTime/Resources/Images.xcassets/AppIcon.appiconset/AppIcon60@2x.png -------------------------------------------------------------------------------- /PopcornTime/Resources/Images.xcassets/AppIcon.appiconset/AppIcon76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danylokos/popcorntime-ios/555aff8d10e38870ef58e75c1af35e5494547c98/PopcornTime/Resources/Images.xcassets/AppIcon.appiconset/AppIcon76.png -------------------------------------------------------------------------------- /PopcornTime/Resources/Images.xcassets/AppIcon.appiconset/AppIcon76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danylokos/popcorntime-ios/555aff8d10e38870ef58e75c1af35e5494547c98/PopcornTime/Resources/Images.xcassets/AppIcon.appiconset/AppIcon76@2x.png -------------------------------------------------------------------------------- /PopcornTime/Resources/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "60x60", 35 | "idiom" : "iphone", 36 | "filename" : "AppIcon60@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "idiom" : "iphone", 41 | "size" : "60x60", 42 | "scale" : "3x" 43 | }, 44 | { 45 | "idiom" : "ipad", 46 | "size" : "20x20", 47 | "scale" : "1x" 48 | }, 49 | { 50 | "idiom" : "ipad", 51 | "size" : "20x20", 52 | "scale" : "2x" 53 | }, 54 | { 55 | "idiom" : "ipad", 56 | "size" : "29x29", 57 | "scale" : "1x" 58 | }, 59 | { 60 | "idiom" : "ipad", 61 | "size" : "29x29", 62 | "scale" : "2x" 63 | }, 64 | { 65 | "idiom" : "ipad", 66 | "size" : "40x40", 67 | "scale" : "1x" 68 | }, 69 | { 70 | "idiom" : "ipad", 71 | "size" : "40x40", 72 | "scale" : "2x" 73 | }, 74 | { 75 | "size" : "76x76", 76 | "idiom" : "ipad", 77 | "filename" : "AppIcon76.png", 78 | "scale" : "1x" 79 | }, 80 | { 81 | "size" : "76x76", 82 | "idiom" : "ipad", 83 | "filename" : "AppIcon76@2x.png", 84 | "scale" : "2x" 85 | }, 86 | { 87 | "idiom" : "ipad", 88 | "size" : "83.5x83.5", 89 | "scale" : "2x" 90 | } 91 | ], 92 | "info" : { 93 | "version" : 1, 94 | "author" : "xcode" 95 | } 96 | } -------------------------------------------------------------------------------- /PopcornTime/Resources/Images.xcassets/BigLogo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x", 10 | "filename" : "popcorn-time-logo.png" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /PopcornTime/Resources/Images.xcassets/BigLogo.imageset/popcorn-time-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danylokos/popcorntime-ios/555aff8d10e38870ef58e75c1af35e5494547c98/PopcornTime/Resources/Images.xcassets/BigLogo.imageset/popcorn-time-logo.png -------------------------------------------------------------------------------- /PopcornTime/Resources/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /PopcornTime/Resources/Images.xcassets/SubwayIconSet/AddToFavoritesIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x", 10 | "filename" : "icon_087@2x.png" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /PopcornTime/Resources/Images.xcassets/SubwayIconSet/AddToFavoritesIcon.imageset/icon_087@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danylokos/popcorntime-ios/555aff8d10e38870ef58e75c1af35e5494547c98/PopcornTime/Resources/Images.xcassets/SubwayIconSet/AddToFavoritesIcon.imageset/icon_087@2x.png -------------------------------------------------------------------------------- /PopcornTime/Resources/Images.xcassets/SubwayIconSet/FavoritesIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x", 10 | "filename" : "icon_086@2x.png" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /PopcornTime/Resources/Images.xcassets/SubwayIconSet/FavoritesIcon.imageset/icon_086@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danylokos/popcorntime-ios/555aff8d10e38870ef58e75c1af35e5494547c98/PopcornTime/Resources/Images.xcassets/SubwayIconSet/FavoritesIcon.imageset/icon_086@2x.png -------------------------------------------------------------------------------- /PopcornTime/Resources/Images.xcassets/SubwayIconSet/MoviesIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x", 10 | "filename" : "icon_0281@2x.png" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /PopcornTime/Resources/Images.xcassets/SubwayIconSet/MoviesIcon.imageset/icon_0281@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danylokos/popcorntime-ios/555aff8d10e38870ef58e75c1af35e5494547c98/PopcornTime/Resources/Images.xcassets/SubwayIconSet/MoviesIcon.imageset/icon_0281@2x.png -------------------------------------------------------------------------------- /PopcornTime/Resources/Images.xcassets/SubwayIconSet/RemoveFromFavoritesIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x", 10 | "filename" : "icon_088@2x.png" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /PopcornTime/Resources/Images.xcassets/SubwayIconSet/RemoveFromFavoritesIcon.imageset/icon_088@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danylokos/popcorntime-ios/555aff8d10e38870ef58e75c1af35e5494547c98/PopcornTime/Resources/Images.xcassets/SubwayIconSet/RemoveFromFavoritesIcon.imageset/icon_088@2x.png -------------------------------------------------------------------------------- /PopcornTime/Resources/Images.xcassets/SubwayIconSet/SettingsIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x", 10 | "filename" : "icon_0186@2x.png" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /PopcornTime/Resources/Images.xcassets/SubwayIconSet/SettingsIcon.imageset/icon_0186@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danylokos/popcorntime-ios/555aff8d10e38870ef58e75c1af35e5494547c98/PopcornTime/Resources/Images.xcassets/SubwayIconSet/SettingsIcon.imageset/icon_0186@2x.png -------------------------------------------------------------------------------- /PopcornTime/Resources/Images.xcassets/SubwayIconSet/ShowsIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x", 10 | "filename" : "icon_0304@2x.png" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /PopcornTime/Resources/Images.xcassets/SubwayIconSet/ShowsIcon.imageset/icon_0304@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danylokos/popcorntime-ios/555aff8d10e38870ef58e75c1af35e5494547c98/PopcornTime/Resources/Images.xcassets/SubwayIconSet/ShowsIcon.imageset/icon_0304@2x.png -------------------------------------------------------------------------------- /PopcornTime/Resources/Launch Screen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /PopcornTime/Resources/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danylokos/popcorntime-ios/555aff8d10e38870ef58e75c1af35e5494547c98/PopcornTime/Resources/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /PopcornTime/Views/EpisodeCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EpisodeCell.swift 3 | // PopcornTime 4 | // 5 | // Created by Andrew K. on 3/13/15. 6 | // Copyright (c) 2015 PopcornTime. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class EpisodeCell: UICollectionViewCell { 12 | 13 | let watchedAlpha:CGFloat = 0.5 14 | let defaultAlpha:CGFloat = 1.0 15 | 16 | @IBOutlet weak var titleLabel: UILabel! 17 | var watchedEpisode = false { 18 | didSet { 19 | if watchedEpisode { 20 | alpha = watchedAlpha 21 | } else { 22 | alpha = defaultAlpha 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /PopcornTime/Views/EpisodeCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /PopcornTime/Views/MoreShowsCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MoreShowsCollectionViewCell.swift 3 | // PopcornTime 4 | // 5 | // Created by Andrew K. on 3/10/15. 6 | // Copyright (c) 2015 PopcornTime. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class MoreShowsCollectionViewCell: UICollectionViewCell { 12 | 13 | override func awakeFromNib() { 14 | super.awakeFromNib() 15 | // Initialization code 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /PopcornTime/Views/MoreShowsCollectionViewCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /PopcornTime/Views/SeasonHeader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SeasonHeader.swift 3 | // PopcornTime 4 | // 5 | // Created by Andrew K. on 4/14/15. 6 | // Copyright (c) 2015 PopcornTime. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SeasonHeader: UICollectionReusableView { 12 | 13 | @IBOutlet weak var container: UIView! 14 | @IBOutlet weak var titleLabel: UILabel! 15 | 16 | } 17 | -------------------------------------------------------------------------------- /PopcornTime/Views/SeasonHeader.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /PopcornTime/Views/ShowCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShowCollectionViewCell.swift 3 | // PopcornTime 4 | // 5 | // Created by Andrew K. on 3/8/15. 6 | // Copyright (c) 2015 PopcornTime. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ShowCollectionViewCell: UICollectionViewCell { 12 | 13 | @IBOutlet weak fileprivate var imageView: UIImageView! 14 | @IBOutlet weak fileprivate var titleLabel: UILabel! 15 | 16 | var image: UIImage? { 17 | didSet { 18 | self.imageView?.image = image 19 | self.titleLabel.isHidden = image != nil 20 | } 21 | } 22 | 23 | var title: String? { 24 | didSet { 25 | titleLabel.text = title 26 | } 27 | } 28 | 29 | override func prepareForReuse() { 30 | super.prepareForReuse() 31 | 32 | self.image = nil 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /PopcornTime/Views/ShowCollectionViewCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /PopcornTime/Views/StratchyHeader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StratchyHeader.swift 3 | // PopcornTime 4 | // 5 | // Created by Andrew K. on 4/6/15. 6 | // Copyright (c) 2015 PopcornTime. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol StratchyHeaderDelegate: class { 12 | ///Triggers when header max stratch value is recalculated 13 | func stratchyHeader(_ header: StratchyHeader, didResetMaxStratchValue value: CGFloat) 14 | } 15 | 16 | 17 | class StratchyHeader: UICollectionReusableView { 18 | 19 | weak var delegate: StratchyHeaderDelegate? 20 | 21 | // MARK: - Public API 22 | var image: UIImage? { 23 | didSet { 24 | if let image = image { 25 | imageAspectRatio = image.size.height / image.size.width 26 | backgroundImageView.image = image 27 | 28 | updateImageViewConstraints() 29 | } 30 | } 31 | } 32 | 33 | var headerSize: CGSize = CGSize(width: 1, height: 1) { 34 | didSet { 35 | updateImageViewConstraints() 36 | } 37 | } 38 | 39 | // MARK: - UICollectionReusableView 40 | override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { 41 | super.apply(layoutAttributes) 42 | 43 | let attributes = layoutAttributes as! StratchyLayoutAttributes 44 | 45 | let height = attributes.frame.height 46 | if (previousHeight != height) { 47 | 48 | if (maxStratch != 0) { 49 | let alpha = 1 - attributes.deltaY / maxStratch 50 | foregroundView.alpha = alpha 51 | } 52 | 53 | if (imageAspectRatio != 0) { 54 | heightConstraint.constant = imageViewActualHeight - attributes.deltaY 55 | widthConstraint.constant = imageViewActualWidth - (attributes.deltaY / imageAspectRatio) 56 | } 57 | previousHeight = height 58 | } 59 | } 60 | 61 | // MARK: - Private 62 | @IBOutlet weak fileprivate var widthConstraint: NSLayoutConstraint! 63 | @IBOutlet weak fileprivate var heightConstraint: NSLayoutConstraint! 64 | @IBOutlet weak fileprivate var backgroundImageView: UIImageView! 65 | 66 | @IBOutlet weak var foregroundView: UIView! 67 | @IBOutlet weak var foregroundImage: UIImageView! 68 | @IBOutlet weak var synopsisTextView: UILabel! 69 | 70 | 71 | fileprivate var maxStratch: CGFloat = 0 72 | 73 | fileprivate var zoomWidthCoef: CGFloat { 74 | get { 75 | let headerAspectRatio = headerSize.height / headerSize.width 76 | return (1.7 * headerAspectRatio) / 0.5325 // Experimentally calculated value :] 77 | } 78 | } 79 | fileprivate var imageAspectRatio: CGFloat = 0 80 | fileprivate var imageViewActualWidth: CGFloat { 81 | return headerSize.width * zoomWidthCoef 82 | } 83 | fileprivate var imageViewActualHeight: CGFloat { 84 | return imageViewActualWidth * imageAspectRatio 85 | } 86 | fileprivate var previousHeight: CGFloat = 0 87 | 88 | fileprivate func updateImageViewConstraints() { 89 | 90 | // let dX = fabs(headerSize.height - imageViewActualHeight)/2 91 | let dY = fabs(headerSize.width - imageViewActualWidth)/2 92 | maxStratch = dY//max(dX, dY) 93 | self.delegate?.stratchyHeader(self, didResetMaxStratchValue: maxStratch) 94 | 95 | widthConstraint.constant = imageViewActualWidth 96 | heightConstraint.constant = imageViewActualHeight 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /PopcornTime/Views/StratchyHeader.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /PopcornTime/Views/StratchyHeaderLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StratchyHeaderLayout.swift 3 | // PopcornTime 4 | // 5 | // Created by Andrew K. on 3/13/15. 6 | // Copyright (c) 2015 PopcornTime. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class StratchyLayoutAttributes: UICollectionViewLayoutAttributes { 12 | 13 | var deltaY: CGFloat = 0 14 | var maxDelta: CGFloat = CGFloat.greatestFiniteMagnitude 15 | 16 | override func copy(with zone: NSZone?) -> Any { 17 | let copy = super.copy(with: zone) as! StratchyLayoutAttributes 18 | copy.deltaY = deltaY 19 | return copy 20 | } 21 | 22 | override func isEqual(_ object: Any?) -> Bool { 23 | if let attributes = object as? StratchyLayoutAttributes { 24 | if attributes.deltaY == deltaY { 25 | return super.isEqual(object) 26 | } 27 | } 28 | return false 29 | } 30 | } 31 | 32 | class StratchyHeaderLayout: UICollectionViewFlowLayout, StratchyHeaderDelegate { 33 | 34 | var headerSize = CGSize.zero 35 | var maxDelta: CGFloat = CGFloat.greatestFiniteMagnitude 36 | let minCellWidth: CGFloat = 300 37 | let cellAspectRatio: CGFloat = 370/46 38 | 39 | override class var layoutAttributesClass : AnyClass { 40 | return StratchyLayoutAttributes.self 41 | } 42 | 43 | override var collectionViewContentSize : CGSize { 44 | 45 | sectionInset.bottom = 15.0 46 | sectionInset.top = 5.0 47 | 48 | minimumInteritemSpacing = sectionInset.left 49 | 50 | let width = self.collectionView!.bounds.width - sectionInset.left - sectionInset.right 51 | let numberOfColumns = Int(width / minCellWidth) 52 | let cellWidth = CGFloat((Int(width) - Int(minimumInteritemSpacing) * (numberOfColumns - 1)) / numberOfColumns) 53 | let cellHeight = cellWidth / cellAspectRatio 54 | self.itemSize = CGSize(width: cellWidth, height: cellHeight) 55 | 56 | return super.collectionViewContentSize 57 | } 58 | 59 | override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { 60 | return true 61 | } 62 | 63 | override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { 64 | 65 | let insets = collectionView!.contentInset 66 | let offset = collectionView!.contentOffset 67 | let minY = -insets.top 68 | 69 | let attributes = super.layoutAttributesForElements(in: rect) 70 | 71 | if let stratchyAttributes = attributes as? [StratchyLayoutAttributes] { 72 | // Check if we've pulled below past the lowest position 73 | if (offset.y < minY){ 74 | let deltaY = fabs(offset.y - minY) 75 | 76 | for attribute in stratchyAttributes{ 77 | if (attribute.indexPath.section == 0){ 78 | if let kind = attribute.representedElementKind{ 79 | if (kind == UICollectionElementKindSectionHeader) { 80 | var headerRect = attribute.frame 81 | headerRect.size.height = min(headerSize.height + maxDelta, max(minY, headerSize.height + deltaY)); 82 | headerRect.origin.y = headerRect.minY - deltaY; 83 | attribute.frame = headerRect 84 | attribute.deltaY = deltaY 85 | attribute.maxDelta = maxDelta 86 | } 87 | } 88 | } 89 | } 90 | } 91 | } 92 | 93 | return attributes 94 | } 95 | 96 | // MARK: - StratchyHeaderDelegate 97 | func stratchyHeader(_ header: StratchyHeader, didResetMaxStratchValue value: CGFloat) { 98 | maxDelta = value 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## PopcornTime for iOS 2 | 3 | [![Build Status](https://www.bitrise.io/app/9ee06c0598c7cbdb.svg?token=nH-7MkkoZ7EpSlvMce4KkA)](https://www.bitrise.io/app/9ee06c0598c7cbdb) 4 | 5 | Version of PopcornTime app for iOS based on [libtorrent](http://www.libtorrent.org) and [MobileVLCKit](https://wiki.videolan.org/VLCKit/). There is still a lot of work to do, but in most cases it works. 6 | 7 | ### Screenshots 8 | 9 | ![](https://raw.github.com/danylokostyshyn/popcorntime-ios/master/Screenshots/1.png) 10 | ![](https://raw.github.com/danylokostyshyn/popcorntime-ios/master/Screenshots/2.png) 11 | ![](https://raw.github.com/danylokostyshyn/popcorntime-ios/master/Screenshots/3.png) 12 | 13 | ### Getting Started 14 | 15 | This project uses [CocoaPods](http://cocoapods.org/). 16 | 17 | ``` bash 18 | $ git clone https://github.com/danylokostyshyn/popcorntime-ios.git 19 | $ cd popcorntime-ios/ 20 | $ pod install 21 | $ open PopcornTime.xcworkspace/ 22 | ``` -------------------------------------------------------------------------------- /Screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danylokos/popcorntime-ios/555aff8d10e38870ef58e75c1af35e5494547c98/Screenshots/1.png -------------------------------------------------------------------------------- /Screenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danylokos/popcorntime-ios/555aff8d10e38870ef58e75c1af35e5494547c98/Screenshots/2.png -------------------------------------------------------------------------------- /Screenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danylokos/popcorntime-ios/555aff8d10e38870ef58e75c1af35e5494547c98/Screenshots/3.png -------------------------------------------------------------------------------- /Thirdparties/VLCKit/Dropin-Player/VDLPlaybackViewController.h: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2013, Felix Paul Kühne and VideoLAN 2 | * All rights reserved. 3 | * 4 | * Redistribution and use in source and binary forms, with or without 5 | * modification, are permitted provided that the following conditions are met: 6 | * 7 | * 1. Redistributions of source code must retain the above copyright notice, 8 | * this list of conditions and the following disclaimer. 9 | * 10 | * 2. Redistributions in binary form must reproduce the above copyright notice, 11 | * this list of conditions and the following disclaimer in the documentation 12 | * and/or other materials provided with the distribution. 13 | * 14 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 18 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 19 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 20 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 21 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 22 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 23 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | * POSSIBILITY OF SUCH DAMAGE. */ 25 | 26 | #import 27 | #import 28 | 29 | @class VDLPlaybackViewController; 30 | @protocol VDLPlaybackViewControllerDelegate 31 | - (void)playbackControllerDidFinishPlayback:(VDLPlaybackViewController *)playbackController; 32 | @end 33 | 34 | @interface VDLPlaybackViewController : UIViewController 35 | 36 | @property (nonatomic, strong) IBOutlet UIView *movieView; 37 | @property (nonatomic, strong) IBOutlet UISlider *positionSlider; 38 | @property (nonatomic, strong) IBOutlet UIButton *timeDisplay; 39 | @property (nonatomic, strong) IBOutlet UIButton *playPauseButton; 40 | @property (nonatomic, strong) IBOutlet UIButton *subtitleSwitcherButton; 41 | @property (nonatomic, strong) IBOutlet UIButton *audioSwitcherButton; 42 | @property (nonatomic, strong) IBOutlet UINavigationBar *toolbar; 43 | @property (nonatomic, strong) IBOutlet UIView *controllerPanel; 44 | @property (nonatomic, strong) IBOutlet MPVolumeView *volumeView; 45 | @property (nonatomic, weak) id delegate; 46 | 47 | - (void)playMediaFromURL:(NSURL*)theURL; 48 | 49 | - (IBAction)closePlayback:(id)sender; 50 | 51 | - (IBAction)positionSliderDrag:(id)sender; 52 | - (IBAction)positionSliderAction:(id)sender; 53 | - (IBAction)toggleTimeDisplay:(id)sender; 54 | 55 | - (IBAction)playandPause:(id)sender; 56 | - (IBAction)switchAudioTrack:(id)sender; 57 | - (IBAction)switchSubtitleTrack:(id)sender; 58 | - (IBAction)switchVideoDimensions:(id)sender; 59 | 60 | @end 61 | -------------------------------------------------------------------------------- /Thirdparties/VLCKit/Dropin-Player/VDLPlaybackViewController.m: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2013, Felix Paul Kühne and VideoLAN 2 | * All rights reserved. 3 | * 4 | * Redistribution and use in source and binary forms, with or without 5 | * modification, are permitted provided that the following conditions are met: 6 | * 7 | * 1. Redistributions of source code must retain the above copyright notice, 8 | * this list of conditions and the following disclaimer. 9 | * 10 | * 2. Redistributions in binary form must reproduce the above copyright notice, 11 | * this list of conditions and the following disclaimer in the documentation 12 | * and/or other materials provided with the distribution. 13 | * 14 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 18 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 19 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 20 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 21 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 22 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 23 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | * POSSIBILITY OF SUCH DAMAGE. */ 25 | 26 | #import "VDLPlaybackViewController.h" 27 | #import 28 | #import 29 | 30 | @interface VDLPlaybackViewController () 31 | { 32 | VLCMediaPlayer *_mediaplayer; 33 | BOOL _setPosition; 34 | BOOL _displayRemainingTime; 35 | int _currentAspectRatioMask; 36 | NSArray *_aspectRatios; 37 | UIActionSheet *_audiotrackActionSheet; 38 | UIActionSheet *_subtitleActionSheet; 39 | NSURL *_url; 40 | NSTimer *_idleTimer; 41 | } 42 | 43 | @end 44 | 45 | @implementation VDLPlaybackViewController 46 | 47 | - (void)viewDidLoad 48 | { 49 | [super viewDidLoad]; 50 | 51 | /* fix-up UI */ 52 | self.wantsFullScreenLayout = YES; 53 | [[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleLightContent]; 54 | 55 | /* we want to influence the system volume */ 56 | [[AVAudioSession sharedInstance] setDelegate:self]; 57 | 58 | /* populate array of supported aspect ratios (there are more!) */ 59 | _aspectRatios = @[@"DEFAULT", @"FILL_TO_SCREEN", @"4:3", @"16:9", @"16:10", @"2.21:1"]; 60 | 61 | /* fix-up the UI */ 62 | CGRect rect = self.toolbar.frame; 63 | rect.size.height += 20.; 64 | self.toolbar.frame = rect; 65 | [self.timeDisplay setTitle:@"" forState:UIControlStateNormal]; 66 | 67 | /* this looks a bit weird, but let's try to support iOS 5 */ 68 | UISlider *volumeSlider = nil; 69 | for (id aView in self.volumeView.subviews){ 70 | if ([[[aView class] description] isEqualToString:@"MPVolumeSlider"]){ 71 | volumeSlider = (UISlider *)aView; 72 | break; 73 | } 74 | } 75 | [volumeSlider addTarget:self 76 | action:@selector(volumeSliderAction:) 77 | forControlEvents:UIControlEventValueChanged]; 78 | 79 | /* setup gesture recognizer to toggle controls' visibility */ 80 | UITapGestureRecognizer *tapOnVideoRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(toggleControlsVisible)]; 81 | [self.movieView addGestureRecognizer:tapOnVideoRecognizer]; 82 | } 83 | 84 | - (void)playMediaFromURL:(NSURL*)theURL 85 | { 86 | _url = theURL; 87 | } 88 | 89 | - (IBAction)playandPause:(id)sender 90 | { 91 | if (_mediaplayer.isPlaying) 92 | [_mediaplayer pause]; 93 | 94 | [_mediaplayer play]; 95 | } 96 | 97 | - (IBAction)closePlayback:(id)sender 98 | { 99 | [self.delegate playbackControllerDidFinishPlayback:self]; 100 | self.delegate = nil; 101 | } 102 | 103 | - (void)viewWillAppear:(BOOL)animated 104 | { 105 | [super viewWillAppear:animated]; 106 | 107 | [self.navigationController setNavigationBarHidden:YES animated:YES]; 108 | 109 | /* setup the media player instance, give it a delegate and something to draw into */ 110 | // NSString *documents = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject; 111 | // NSString *logFilePath = [documents stringByAppendingPathComponent:@"log.txt"]; 112 | // NSString *logParam = [NSString stringWithFormat:@"--logfile=\"%@\"", logFilePath]; 113 | _mediaplayer = [[VLCMediaPlayer alloc] initWithOptions:@[@"--avi-index=2", @"--play-and-pause"]]; 114 | _mediaplayer.delegate = self; 115 | _mediaplayer.drawable = self.movieView; 116 | 117 | /* listen for notifications from the player */ 118 | [_mediaplayer addObserver:self forKeyPath:@"time" options:0 context:nil]; 119 | [_mediaplayer addObserver:self forKeyPath:@"remainingTime" options:0 context:nil]; 120 | 121 | /* create a media object and give it to the player */ 122 | _mediaplayer.media = [VLCMedia mediaWithURL:_url]; 123 | 124 | [_mediaplayer play]; 125 | 126 | if (self.controllerPanel.hidden) 127 | [self toggleControlsVisible]; 128 | 129 | [self _resetIdleTimer]; 130 | } 131 | 132 | 133 | - (void)viewWillDisappear:(BOOL)animated 134 | { 135 | [super viewWillDisappear:animated]; 136 | 137 | if (_mediaplayer) { 138 | @try { 139 | [_mediaplayer removeObserver:self forKeyPath:@"time"]; 140 | [_mediaplayer removeObserver:self forKeyPath:@"remainingTime"]; 141 | } 142 | @catch (NSException *exception) { 143 | NSLog(@"we weren't an observer yet"); 144 | } 145 | 146 | if (_mediaplayer.media) 147 | [_mediaplayer stop]; 148 | 149 | if (_mediaplayer) 150 | _mediaplayer = nil; 151 | } 152 | 153 | if (_idleTimer) { 154 | [_idleTimer invalidate]; 155 | _idleTimer = nil; 156 | } 157 | 158 | [self.navigationController setNavigationBarHidden:NO animated:YES]; 159 | [[UIApplication sharedApplication] setStatusBarHidden:NO withAnimation:UIStatusBarAnimationFade]; 160 | } 161 | 162 | - (IBAction)positionSliderAction:(UISlider *)sender 163 | { 164 | [self _resetIdleTimer]; 165 | 166 | /* we need to limit the number of events sent by the slider, since otherwise, the user 167 | * wouldn't see the I-frames when seeking on current mobile devices. This isn't a problem 168 | * within the Simulator, but especially on older ARMv7 devices, it's clearly noticeable. */ 169 | [self performSelector:@selector(_setPositionForReal) withObject:nil afterDelay:0.3]; 170 | _setPosition = NO; 171 | } 172 | 173 | - (void)_setPositionForReal 174 | { 175 | if (!_setPosition) { 176 | _mediaplayer.position = _positionSlider.value; 177 | _setPosition = YES; 178 | } 179 | } 180 | 181 | - (IBAction)positionSliderDrag:(id)sender 182 | { 183 | [self _resetIdleTimer]; 184 | } 185 | 186 | - (IBAction)volumeSliderAction:(id)sender 187 | { 188 | [self _resetIdleTimer]; 189 | } 190 | 191 | - (void)mediaPlayerStateChanged:(NSNotification *)aNotification 192 | { 193 | VLCMediaPlayerState currentState = _mediaplayer.state; 194 | 195 | /* distruct view controller on error */ 196 | if (currentState == VLCMediaPlayerStateError) 197 | [self performSelector:@selector(closePlayback:) withObject:nil afterDelay:2.]; 198 | 199 | /* or if playback ended */ 200 | if (currentState == VLCMediaPlayerStateEnded || currentState == VLCMediaPlayerStateStopped) 201 | [self performSelector:@selector(closePlayback:) withObject:nil afterDelay:2.]; 202 | 203 | [self.playPauseButton setTitle:[_mediaplayer isPlaying]? @"Pause" : @"Play" forState:UIControlStateNormal]; 204 | } 205 | 206 | - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context 207 | { 208 | self.positionSlider.value = [_mediaplayer position]; 209 | 210 | if (_displayRemainingTime) 211 | [self.timeDisplay setTitle:[[_mediaplayer remainingTime] stringValue] forState:UIControlStateNormal]; 212 | else 213 | [self.timeDisplay setTitle:[[_mediaplayer time] stringValue] forState:UIControlStateNormal]; 214 | } 215 | 216 | - (IBAction)toggleTimeDisplay:(id)sender 217 | { 218 | [self _resetIdleTimer]; 219 | _displayRemainingTime = !_displayRemainingTime; 220 | } 221 | 222 | - (void)toggleControlsVisible 223 | { 224 | BOOL controlsHidden = !self.controllerPanel.hidden; 225 | self.controllerPanel.hidden = controlsHidden; 226 | self.toolbar.hidden = controlsHidden; 227 | [[UIApplication sharedApplication] setStatusBarHidden:controlsHidden withAnimation:UIStatusBarAnimationFade]; 228 | } 229 | 230 | - (void)_resetIdleTimer 231 | { 232 | if (!_idleTimer) 233 | _idleTimer = [NSTimer scheduledTimerWithTimeInterval:5. 234 | target:self 235 | selector:@selector(idleTimerExceeded) 236 | userInfo:nil 237 | repeats:NO]; 238 | else { 239 | if (fabs([_idleTimer.fireDate timeIntervalSinceNow]) < 5.) 240 | [_idleTimer setFireDate:[NSDate dateWithTimeIntervalSinceNow:5.]]; 241 | } 242 | } 243 | 244 | - (void)idleTimerExceeded 245 | { 246 | _idleTimer = nil; 247 | 248 | if (!self.controllerPanel.hidden) 249 | [self toggleControlsVisible]; 250 | } 251 | 252 | - (IBAction)switchVideoDimensions:(id)sender 253 | { 254 | [self _resetIdleTimer]; 255 | 256 | NSUInteger count = [_aspectRatios count]; 257 | 258 | if (_currentAspectRatioMask + 1 > count - 1) { 259 | _mediaplayer.videoAspectRatio = NULL; 260 | _mediaplayer.videoCropGeometry = NULL; 261 | _currentAspectRatioMask = 0; 262 | NSLog(@"crop disabled"); 263 | } else { 264 | _currentAspectRatioMask++; 265 | 266 | if ([_aspectRatios[_currentAspectRatioMask] isEqualToString:@"FILL_TO_SCREEN"]) { 267 | UIScreen *screen = [UIScreen mainScreen]; 268 | float f_ar = screen.bounds.size.width / screen.bounds.size.height; 269 | 270 | if (f_ar == (float)(640./1136.)) // iPhone 5 aka 16:9.01 271 | _mediaplayer.videoCropGeometry = "16:9"; 272 | else if (f_ar == (float)(2./3.)) // all other iPhones 273 | _mediaplayer.videoCropGeometry = "16:10"; // libvlc doesn't support 2:3 crop 274 | else if (f_ar == .75) // all iPads 275 | _mediaplayer.videoCropGeometry = "4:3"; 276 | else if (f_ar == .5625) // AirPlay 277 | _mediaplayer.videoCropGeometry = "16:9"; 278 | else 279 | NSLog(@"unknown screen format %f, can't crop", f_ar); 280 | 281 | NSLog(@"FILL_TO_SCREEN"); 282 | return; 283 | } 284 | 285 | _mediaplayer.videoCropGeometry = NULL; 286 | _mediaplayer.videoAspectRatio = (char *)[_aspectRatios[_currentAspectRatioMask] UTF8String]; 287 | NSLog(@"crop switched to %@", _aspectRatios[_currentAspectRatioMask]); 288 | } 289 | } 290 | 291 | - (IBAction)switchAudioTrack:(id)sender 292 | { 293 | _audiotrackActionSheet = [[UIActionSheet alloc] initWithTitle:@"audio track selector" delegate:self cancelButtonTitle:nil destructiveButtonTitle:nil otherButtonTitles: nil]; 294 | NSArray *audioTracks = [_mediaplayer audioTrackNames]; 295 | NSArray *audioTrackIndexes = [_mediaplayer audioTrackIndexes]; 296 | 297 | NSUInteger count = [audioTracks count]; 298 | for (NSUInteger i = 0; i < count; i++) { 299 | NSString *indexIndicator = ([audioTrackIndexes[i] intValue] == [_mediaplayer currentAudioTrackIndex])? @"\u2713": @""; 300 | NSString *buttonTitle = [NSString stringWithFormat:@"%@ %@", indexIndicator, audioTracks[i]]; 301 | [_audiotrackActionSheet addButtonWithTitle:buttonTitle]; 302 | } 303 | 304 | [_audiotrackActionSheet addButtonWithTitle:@"Cancel"]; 305 | [_audiotrackActionSheet setCancelButtonIndex:[_audiotrackActionSheet numberOfButtons] - 1]; 306 | [_audiotrackActionSheet showInView:self.audioSwitcherButton]; 307 | } 308 | 309 | - (IBAction)switchSubtitleTrack:(id)sender 310 | { 311 | NSArray *spuTracks = [_mediaplayer videoSubTitlesNames]; 312 | NSArray *spuTrackIndexes = [_mediaplayer videoSubTitlesIndexes]; 313 | 314 | NSUInteger count = [spuTracks count]; 315 | if (count <= 1) 316 | return; 317 | _subtitleActionSheet = [[UIActionSheet alloc] initWithTitle:@"subtitle track selector" delegate:self cancelButtonTitle:nil destructiveButtonTitle:nil otherButtonTitles: nil]; 318 | 319 | for (NSUInteger i = 0; i < count; i++) { 320 | NSString *indexIndicator = ([spuTrackIndexes[i] intValue] == [_mediaplayer currentVideoSubTitleIndex])? @"\u2713": @""; 321 | NSString *buttonTitle = [NSString stringWithFormat:@"%@ %@", indexIndicator, spuTracks[i]]; 322 | [_subtitleActionSheet addButtonWithTitle:buttonTitle]; 323 | } 324 | 325 | [_subtitleActionSheet addButtonWithTitle:@"Cancel"]; 326 | [_subtitleActionSheet setCancelButtonIndex:[_subtitleActionSheet numberOfButtons] - 1]; 327 | [_subtitleActionSheet showInView: self.subtitleSwitcherButton]; 328 | } 329 | 330 | - (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex { 331 | if (buttonIndex == [actionSheet cancelButtonIndex]) 332 | return; 333 | 334 | NSArray *indexArray; 335 | if (actionSheet == _subtitleActionSheet) { 336 | indexArray = _mediaplayer.videoSubTitlesIndexes; 337 | if (buttonIndex <= indexArray.count) { 338 | _mediaplayer.currentVideoSubTitleIndex = [indexArray[buttonIndex] intValue]; 339 | } 340 | } else if (actionSheet == _audiotrackActionSheet) { 341 | indexArray = _mediaplayer.audioTrackIndexes; 342 | if (buttonIndex <= indexArray.count) { 343 | _mediaplayer.currentAudioTrackIndex = [indexArray[buttonIndex] intValue]; 344 | } 345 | } 346 | } 347 | 348 | - (void)didReceiveMemoryWarning 349 | { 350 | [super didReceiveMemoryWarning]; 351 | // Dispose of any resources that can be recreated. 352 | } 353 | 354 | @end 355 | --------------------------------------------------------------------------------