├── .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 | [](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 | 
10 | 
11 | 
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 |
--------------------------------------------------------------------------------