├── README.md ├── Releases └── WWDC.srt.zip ├── ScreenShots ├── Session01.png ├── Session02.png ├── TextFile.png └── VideoLink.png ├── WWDC.srt.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcuserdata │ │ └── SeyedSamad.xcuserdatad │ │ └── IDEFindNavigatorScopes.plist ├── xcshareddata │ ├── xcbaselines │ │ └── E4CDBE55220BF3F800CE6A9C.xcbaseline │ │ │ ├── F17008CD-99B2-4B1D-8F0F-7472A553CA00.plist │ │ │ └── Info.plist │ └── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist └── xcuserdata │ └── SeyedSamad.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ ├── WWDCSubGetter.xcscheme │ └── xcschememanagement.plist ├── WWDC.srtTests ├── 505.html ├── 708.html ├── Info.plist └── WWDC_srtTests.swift └── WWDCSubGetter ├── AppDelegate.swift ├── Array+Extensions.swift ├── Assets.xcassets ├── AppIcon.appiconset │ ├── Contents.json │ ├── Icon_128x128.png │ ├── Icon_128x128@2x.png │ ├── Icon_16x16.png │ ├── Icon_16x16@2x.png │ ├── Icon_256x256.png │ ├── Icon_256x256@2x.png │ ├── Icon_32x32.png │ ├── Icon_32x32@2x.png │ ├── Icon_512x512.png │ └── Icon_512x512@2x.png └── Contents.json ├── Base.lproj └── Main.storyboard ├── ConvertToSubtitleOperation.swift ├── DownloadHtmlVideoPageOperation.swift ├── DownloadM3U8Operation.swift ├── DownloadWebvttOperation.swift ├── ExportLinksOperation.swift ├── ExportSrtFileOperation.swift ├── FetchHtmlVideoPagesOperation.swift ├── FetchSubtitilesOperation.swift ├── FetchWebvttsOperation.swift ├── GetHtmlSessionsListOperation.swift ├── GetLinksOperation.swift ├── GetSessionsListOperation.swift ├── GetSubtitlesOperation.swift ├── Info.plist ├── LinksModel.swift ├── MainViewController.swift ├── Model.swift ├── Operations ├── Conditions │ ├── MutuallyExclusive.swift │ ├── OperationCondition.swift │ ├── OperationErrors.swift │ └── ReachabilityCondition.swift ├── Foundation.Operation+Operation.swift ├── NSLock+Operations.swift ├── Observers │ ├── BlockObserver.swift │ ├── NetworkObserver.swift │ └── OperationObserver.swift ├── OperationQueue │ ├── ExclusivityController.swift │ └── OperationQueue.swift └── Operations │ ├── AlertOperation.swift │ ├── BlockOperation.swift │ ├── GroupOperation.swift │ ├── Operation.swift │ └── URLSessionTaskOperation.swift ├── ParseHtmlVideoPageOperation.swift ├── ParseSessionsListOperation.swift ├── Presenter.swift ├── Resources ├── WWDC2013_links.txt ├── WWDC2014_links.txt ├── WWDC2015_links.txt ├── WWDC2016_links.txt └── WWDC2017_links.txt ├── String+Extensions.swift ├── Subtitle.swift ├── SubtitlesProgress.swift ├── TextFileView.swift ├── WWDC2019.swift ├── WWDC2020.swift ├── WWDC2021.swift ├── WWDCVideosController.swift ├── WWDCYear.swift └── Webvtt.swift /README.md: -------------------------------------------------------------------------------- 1 | ![icon](./WWDCSubGetter/Assets.xcassets/AppIcon.appiconset/Icon_256x256.png) 2 | 3 |

4 | 5 | app version 1.6.4 6 | 7 | 8 | Swift 4.0 9 | 10 | 11 | Platform iOS | macOS 12 | 13 | 14 | ssamadgh Twitter 15 | 16 | 17 | 18 | ssamadgh Linkedin 19 | 20 |

21 | 22 |

23 | 24 | # WWDC.srt 25 | #### An app for Download WWDC subtitles 26 | 27 | ### Whats New: 28 | 29 | **version 1.6.6:** 30 | 31 | - Supports WWDC 2024 32 | 33 | **version 1.6.5:** 34 | 35 | - Supports WWDC 2022 and WWDC 2023 36 | 37 | **version 1.6.4:** 38 | 39 | - Supports WWDC 2020 40 | - Fixed a bug which doesn't download some of subtitles and video links correctly 41 | 42 | **version 1.6.2:** 43 | 44 | - Now can download link of Sample codes for WWDC 2019, 2018, 2017, 2016 videos 45 | - Some minor bugs fixed 46 | 47 | **version 1.6.1:** 48 | 49 | - WWDC 2019 Added. 50 | 51 | **version 1.6.0:** 52 | 53 | - Performance optimized. Now you get links and subtitles much faster. 54 | - Now supports Tech Talks Video's links 55 | 56 | **version 1.5.3:** 57 | 58 | - Fixed an issue that failed to get all links after you selecting a special link to get 59 | 60 | **version 1.5.2:** 61 | 62 | - Fixed an issue that downloads wrong subtitle for selected session in some cases 63 | 64 | **version 1.5.1:** 65 | 66 | - Now supports WWDC 2018 video's links 67 | 68 | 69 | - Now you can get links of videos, pdfs and sample codes for each session you want and even for all sessions at once! 70 | 71 | - Now the app opens the destination address of your desired data in finder, after downloading them. 72 | 73 | - Some minor bugs fixed 74 | 75 | 76 | **version 1.0.1:** 77 | 78 | - Now supports Fall 2017 video's links 79 | 80 | ## Intro 81 | WWDC.srt allows you to download subtitle for each WWDC session video since 2013 in (**srt**) format. 82 | 83 | ⬇️ If you just want to download the latest release, go to [this link](./Releases/WWDC.srt.zip). 84 | 85 | ## Session 86 | 87 | In this tab you can choose ( or search session number of ) your favorite WWDC Session video from the list and download it's subtitle or data links by clicking get button. Also you can download all sessions subtitles or data links alltogether by choosing (All Sessions) radio button. 88 | Data links are include videos links (HD or SD depending on your choice), pdf links and sample code links. 89 | 90 | ![Session](./ScreenShots/Session01.png) 91 | 92 | 93 | ![Session](./ScreenShots/Session02.png) 94 | 95 | 96 | ## Video Link 97 | 98 | In this tab you can paste WWDC video link like: 99 | 100 | ` https://devstreaming-cdn.apple.com/videos/tutorials/20170912/201qy4t11tjpm/building_apps_for_iphone_x/building_apps_for_iphone_x_hd.mp4?dl=1 ` 101 | 102 | into text field and download it's subtitle. 103 | 104 | ![Video Link](./ScreenShots/VideoLink.png) 105 | 106 | 107 | ## Text File 108 | 109 | In this tab you can just drag a text file which contains a bunch of your favorite WWDC Video's links into the view and download their subtitles altogether. 110 | 111 | ![Text File](./ScreenShots/TextFile.png) 112 | 113 | ## Building the app 114 | 115 | **Building requires Xcode 9 or later.** 116 | 117 | Just clone this branch and run the project in xcode 9. 118 | -------------------------------------------------------------------------------- /Releases/WWDC.srt.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssamadgh/WWDCsrt/0fa30d0940420e3039b4cb4d5763eb351f7849d7/Releases/WWDC.srt.zip -------------------------------------------------------------------------------- /ScreenShots/Session01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssamadgh/WWDCsrt/0fa30d0940420e3039b4cb4d5763eb351f7849d7/ScreenShots/Session01.png -------------------------------------------------------------------------------- /ScreenShots/Session02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssamadgh/WWDCsrt/0fa30d0940420e3039b4cb4d5763eb351f7849d7/ScreenShots/Session02.png -------------------------------------------------------------------------------- /ScreenShots/TextFile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssamadgh/WWDCsrt/0fa30d0940420e3039b4cb4d5763eb351f7849d7/ScreenShots/TextFile.png -------------------------------------------------------------------------------- /ScreenShots/VideoLink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssamadgh/WWDCsrt/0fa30d0940420e3039b4cb4d5763eb351f7849d7/ScreenShots/VideoLink.png -------------------------------------------------------------------------------- /WWDC.srt.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /WWDC.srt.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /WWDC.srt.xcodeproj/project.xcworkspace/xcuserdata/SeyedSamad.xcuserdatad/IDEFindNavigatorScopes.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /WWDC.srt.xcodeproj/xcshareddata/xcbaselines/E4CDBE55220BF3F800CE6A9C.xcbaseline/F17008CD-99B2-4B1D-8F0F-7472A553CA00.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | classNames 6 | 7 | WWDC_srtTests 8 | 9 | testExample() 10 | 11 | com.apple.XCTPerformanceMetric_WallClockTime 12 | 13 | baselineAverage 14 | 0.00158 15 | baselineIntegrationDisplayName 16 | Local Baseline 17 | 18 | 19 | testPdfParsing() 20 | 21 | com.apple.XCTPerformanceMetric_WallClockTime 22 | 23 | baselineAverage 24 | 0.0018097 25 | baselineIntegrationDisplayName 26 | Local Baseline 27 | 28 | 29 | testZipParsing() 30 | 31 | com.apple.XCTPerformanceMetric_WallClockTime 32 | 33 | baselineAverage 34 | 0.989 35 | baselineIntegrationDisplayName 36 | Local Baseline 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /WWDC.srt.xcodeproj/xcshareddata/xcbaselines/E4CDBE55220BF3F800CE6A9C.xcbaseline/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | runDestinationsByUUID 6 | 7 | F17008CD-99B2-4B1D-8F0F-7472A553CA00 8 | 9 | localComputer 10 | 11 | busSpeedInMHz 12 | 100 13 | cpuCount 14 | 1 15 | cpuKind 16 | Intel Core i7 17 | cpuSpeedInMHz 18 | 2500 19 | logicalCPUCoresPerPackage 20 | 8 21 | modelCode 22 | MacBookPro11,5 23 | physicalCPUCoresPerPackage 24 | 4 25 | platformIdentifier 26 | com.apple.platform.macosx 27 | 28 | targetArchitecture 29 | x86_64 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /WWDC.srt.xcodeproj/xcshareddata/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 9 | 22 | 23 | 25 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /WWDC.srt.xcodeproj/xcuserdata/SeyedSamad.xcuserdatad/xcschemes/WWDCSubGetter.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 42 | 48 | 49 | 50 | 52 | 58 | 59 | 60 | 61 | 62 | 73 | 75 | 81 | 82 | 83 | 84 | 90 | 92 | 98 | 99 | 100 | 101 | 103 | 104 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /WWDC.srt.xcodeproj/xcuserdata/SeyedSamad.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | WWDCSubGetter.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | E4F327C31F406EA80018B524 16 | 17 | primary 18 | 19 | 20 | E4F327D41F406EA80018B524 21 | 22 | primary 23 | 24 | 25 | E4F327DF1F406EA80018B524 26 | 27 | primary 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /WWDC.srtTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /WWDC.srtTests/WWDC_srtTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WWDC_srtTests.swift 3 | // WWDC.srtTests 4 | // 5 | // Created by Seyed Samad Gholamzadeh on 2/7/19. 6 | // Copyright © 2019 Seyed Samad Gholamzadeh. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import WWDC_srt 11 | 12 | class WWDC_srtTests: XCTestCase { 13 | 14 | let presenter = Presenter() 15 | 16 | override func setUp() { 17 | // Put setup code here. This method is called before the invocation of each test method in the class. 18 | } 19 | 20 | override func tearDown() { 21 | // Put teardown code here. This method is called after the invocation of each test method in the class. 22 | } 23 | 24 | func testVideoParsing() { 25 | let fileName = "708" 26 | let bundle = Bundle(for: type(of: self)) 27 | let url = bundle.url(forResource: fileName, withExtension: "html")! 28 | let data = try! Data(contentsOf: url, options: Data.ReadingOptions.mappedIfSafe) 29 | 30 | let htmlText = String.init(data: data, encoding: 31 | .utf8)! 32 | 33 | self.measure { 34 | let videoURLString = WWDCVideosController.getHDorSDdURLs(fromHTML: htmlText, format: .sd) 35 | print(videoURLString) 36 | // 37 | // let pdfURLStrings = WWDCVideosController.getPDFResourceURL(fromHTML: htmlText) 38 | // let sampleCodesURLStrings = WWDCVideosController.getSampleCodeURL(fromHTML: htmlText) 39 | // let sampleCodesURLStrings2 = WWDCVideosController.getSampleCodeURL2(fromHTML: htmlText) 40 | } 41 | 42 | } 43 | 44 | func testPdfParsing() { 45 | let fileName = "708" 46 | let bundle = Bundle(for: type(of: self)) 47 | let url = bundle.url(forResource: fileName, withExtension: "html")! 48 | let data = try! Data(contentsOf: url, options: Data.ReadingOptions.mappedIfSafe) 49 | 50 | let htmlText = String.init(data: data, encoding: 51 | .utf8)! 52 | 53 | self.measure { 54 | let pdfURLStrings = WWDCVideosController.getPDFResourceURL(fromHTML: htmlText) 55 | print(pdfURLStrings) 56 | 57 | // let sampleCodesURLStrings = WWDCVideosController.getSampleCodeURL(fromHTML: htmlText) 58 | // let sampleCodesURLStrings2 = WWDCVideosController.getSampleCodeURL2(fromHTML: htmlText) 59 | } 60 | } 61 | 62 | func testZipParsing() { 63 | 64 | let fileName = "505" 65 | let bundle = Bundle(for: type(of: self)) 66 | let url = bundle.url(forResource: fileName, withExtension: "html")! 67 | let data = try! Data(contentsOf: url, options: Data.ReadingOptions.mappedIfSafe) 68 | 69 | let htmlText = String.init(data: data, encoding: 70 | .utf8)! 71 | 72 | self.measure { 73 | let sampleCodesURLStrings = WWDCVideosController.getSampleCodeURL(fromHTML: htmlText) 74 | print("sampleCodesURLStrings: ", sampleCodesURLStrings.count) 75 | 76 | let sampleCodesURLStrings2 = WWDCVideosController.getSampleCodeURL2(fromHTML: htmlText) 77 | print("sampleCodesURLStrings2: ", sampleCodesURLStrings2.count) 78 | } 79 | 80 | } 81 | 82 | 83 | } 84 | -------------------------------------------------------------------------------- /WWDCSubGetter/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // WWDC.srt 4 | // 5 | // Created by Seyed Samad Gholamzadeh on 5/22/1396 AP. 6 | // Copyright © 1396 AP Seyed Samad Gholamzadeh. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | @NSApplicationMain 12 | class AppDelegate: NSObject, NSApplicationDelegate { 13 | 14 | 15 | func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { 16 | if !flag { 17 | for window in sender.windows { 18 | window.windowController?.showWindow(self) 19 | } 20 | } 21 | 22 | return true 23 | } 24 | 25 | } 26 | 27 | -------------------------------------------------------------------------------- /WWDCSubGetter/Array+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Array+Extensions.swift 3 | 4 | // 5 | // Created by Seyed Samad Gholamzadeh on 24/7/1396 AP. 6 | // Copyright © 1396 AP Seyed Samad Gholamzadeh. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | extension Array where Element: Hashable { 13 | 14 | /// A method which return unique values of array. 15 | func removingDuplicates() -> [Element] { 16 | var addedDict = [Element: Bool]() 17 | 18 | return filter { 19 | addedDict.updateValue(true, forKey: $0) == nil 20 | } 21 | } 22 | 23 | mutating func removeDuplicates() { 24 | self = self.removingDuplicates() 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /WWDCSubGetter/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "Icon_16x16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "Icon_16x16@2x.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "Icon_32x32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "Icon_32x32@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "Icon_128x128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "Icon_128x128@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "Icon_256x256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "Icon_256x256@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "Icon_512x512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "Icon_512x512@2x.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /WWDCSubGetter/Assets.xcassets/AppIcon.appiconset/Icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssamadgh/WWDCsrt/0fa30d0940420e3039b4cb4d5763eb351f7849d7/WWDCSubGetter/Assets.xcassets/AppIcon.appiconset/Icon_128x128.png -------------------------------------------------------------------------------- /WWDCSubGetter/Assets.xcassets/AppIcon.appiconset/Icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssamadgh/WWDCsrt/0fa30d0940420e3039b4cb4d5763eb351f7849d7/WWDCSubGetter/Assets.xcassets/AppIcon.appiconset/Icon_128x128@2x.png -------------------------------------------------------------------------------- /WWDCSubGetter/Assets.xcassets/AppIcon.appiconset/Icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssamadgh/WWDCsrt/0fa30d0940420e3039b4cb4d5763eb351f7849d7/WWDCSubGetter/Assets.xcassets/AppIcon.appiconset/Icon_16x16.png -------------------------------------------------------------------------------- /WWDCSubGetter/Assets.xcassets/AppIcon.appiconset/Icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssamadgh/WWDCsrt/0fa30d0940420e3039b4cb4d5763eb351f7849d7/WWDCSubGetter/Assets.xcassets/AppIcon.appiconset/Icon_16x16@2x.png -------------------------------------------------------------------------------- /WWDCSubGetter/Assets.xcassets/AppIcon.appiconset/Icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssamadgh/WWDCsrt/0fa30d0940420e3039b4cb4d5763eb351f7849d7/WWDCSubGetter/Assets.xcassets/AppIcon.appiconset/Icon_256x256.png -------------------------------------------------------------------------------- /WWDCSubGetter/Assets.xcassets/AppIcon.appiconset/Icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssamadgh/WWDCsrt/0fa30d0940420e3039b4cb4d5763eb351f7849d7/WWDCSubGetter/Assets.xcassets/AppIcon.appiconset/Icon_256x256@2x.png -------------------------------------------------------------------------------- /WWDCSubGetter/Assets.xcassets/AppIcon.appiconset/Icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssamadgh/WWDCsrt/0fa30d0940420e3039b4cb4d5763eb351f7849d7/WWDCSubGetter/Assets.xcassets/AppIcon.appiconset/Icon_32x32.png -------------------------------------------------------------------------------- /WWDCSubGetter/Assets.xcassets/AppIcon.appiconset/Icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssamadgh/WWDCsrt/0fa30d0940420e3039b4cb4d5763eb351f7849d7/WWDCSubGetter/Assets.xcassets/AppIcon.appiconset/Icon_32x32@2x.png -------------------------------------------------------------------------------- /WWDCSubGetter/Assets.xcassets/AppIcon.appiconset/Icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssamadgh/WWDCsrt/0fa30d0940420e3039b4cb4d5763eb351f7849d7/WWDCSubGetter/Assets.xcassets/AppIcon.appiconset/Icon_512x512.png -------------------------------------------------------------------------------- /WWDCSubGetter/Assets.xcassets/AppIcon.appiconset/Icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssamadgh/WWDCsrt/0fa30d0940420e3039b4cb4d5763eb351f7849d7/WWDCSubGetter/Assets.xcassets/AppIcon.appiconset/Icon_512x512@2x.png -------------------------------------------------------------------------------- /WWDCSubGetter/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /WWDCSubGetter/ConvertToSubtitleOperation.swift: -------------------------------------------------------------------------------- 1 | /* 2 | VerifyVideoURLCondition.swift 3 | WWDC.srt 4 | 5 | Created by Seyed Samad Gholamzadeh on 7/19/1396 AP. 6 | Copyright © 1396 AP Seyed Samad Gholamzadeh. All rights reserved. 7 | 8 | Abstract: 9 | In this file we convert wwdc video links to `Subtitle` objects. 10 | */ 11 | 12 | import Foundation 13 | 14 | /** 15 | This enum is for detecting input url type. We have Two types of input for video links. 16 | A single video link and tex file with a bunch of video links. 17 | */ 18 | enum InputType { 19 | case textFile 20 | case videoLink 21 | } 22 | 23 | /// An `Operation` for validate the wwdc video links and convert them to `Subtitle`. 24 | final class ConvertToSubtitleOperation: Operation { 25 | 26 | let url: String 27 | let wwdc: WWDC 28 | let type: InputType 29 | var completion: (([Subtitle]) -> Void)! 30 | 31 | init(from url: String, wwdc: WWDC, type: InputType) { 32 | self.url = url 33 | self.wwdc = wwdc 34 | self.type = type 35 | 36 | super.init() 37 | self.name = "ConvertToSubtitleOperation" 38 | } 39 | 40 | convenience init(from url: String, wwdc: WWDC, type: InputType, completion: @escaping ([Subtitle]) -> Void) { 41 | self.init(from: url, wwdc: wwdc, type: type) 42 | self.completion = completion 43 | 44 | } 45 | 46 | override func execute() { 47 | 48 | /* 49 | We check validation of video links in each type seperately and put back `Subtitle` 50 | objects of validate links 51 | */ 52 | 53 | switch self.type { 54 | case .videoLink: 55 | 56 | let pattern = ".m[op4][v4]" 57 | if url.contains(pattern) { 58 | let url = self.url.trimmingCharacters(in: .whitespacesAndNewlines) 59 | model.createSubtitle(with: url, wwdc: wwdc) 60 | } 61 | 62 | case .textFile: 63 | let array = convertTextFileToArray() 64 | for url in array { 65 | let url = url.trimmingCharacters(in: .whitespacesAndNewlines) 66 | model.createSubtitle(with: url, wwdc: wwdc) 67 | } 68 | } 69 | 70 | let subtitlesArray = model.allSubtitles() 71 | completion(subtitlesArray) 72 | model.clear() 73 | finish() 74 | } 75 | 76 | /** 77 | This method converts text file to an array of video links were inside of text file. 78 | it also checks if the link is realy blong to a video and filters true links. 79 | */ 80 | func convertTextFileToArray() -> [String] { 81 | 82 | do { 83 | 84 | // This solution assumes you've got the file in your bundle 85 | 86 | let data = try String(contentsOfFile:url, encoding: String.Encoding.utf8) 87 | let pattern = ".m[op4][v4]" 88 | let videosURLArray = data.components(separatedBy: "\n").filter {$0.contains(pattern)} 89 | 90 | return videosURLArray 91 | 92 | } catch { 93 | 94 | // do something with Error 95 | print(error.localizedDescription) 96 | } 97 | return [] 98 | } 99 | 100 | } 101 | 102 | -------------------------------------------------------------------------------- /WWDCSubGetter/DownloadHtmlVideoPageOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetHtmlVideoPageOperation.swift 3 | // WWDC.srt 4 | // 5 | // Created by Seyed Samad Gholamzadeh on 6/6/18. 6 | // Copyright © 2018 Seyed Samad Gholamzadeh. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | final class DownloadHtmlVideoPageOperation: GroupOperation { 12 | 13 | //MARK: Properties 14 | 15 | let cacheFile: URL 16 | 17 | //MARK: Initializer 18 | 19 | /// -parameter cacheFile: The file `URL` to wich the earthquake feed will be downloaded. 20 | init(wwdcYear: WWDC, sessionNumber: String, cacheFile: URL) { 21 | self.cacheFile = cacheFile 22 | super.init(operations: []) 23 | name = "GetHtmlVideoPageOperation \(sessionNumber)" 24 | 25 | let url = URL(string: "https://developer.apple.com/videos/play/\(wwdcYear.stringValue)/" + sessionNumber + "/")! 26 | 27 | let task = URLSession.shared.downloadTask(with: url) { (url, response, error) in 28 | self.downloadFinished(url, response: response, error: error as NSError?) 29 | return() 30 | } 31 | 32 | let taskOperation = URLSessionTaskOperation(task: task) 33 | 34 | let reachabilityCondition = ReachabilityCondition(host: url) 35 | taskOperation.addCondition(reachabilityCondition) 36 | 37 | addOperation(taskOperation) 38 | 39 | } 40 | 41 | func downloadFinished(_ url: URL?, response: URLResponse?, error: NSError?) { 42 | if let localURL = url { 43 | do { 44 | /* 45 | If we already have a file at this location, just delete it. 46 | Also swallow the error, because we don't really care about it. 47 | */ 48 | try FileManager.default.removeItem(at: cacheFile) 49 | } 50 | catch { } 51 | 52 | do { 53 | try FileManager.default.moveItem(at: localURL, to: cacheFile) 54 | } 55 | catch let error as NSError { 56 | aggregateError(error) 57 | } 58 | } 59 | else if let error = error { 60 | aggregateError(error) 61 | } 62 | else { 63 | // Do nothing, and the operation will automatically finish. 64 | } 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /WWDCSubGetter/DownloadM3U8Operation.swift: -------------------------------------------------------------------------------- 1 | /* 2 | DownloadAndMakeSrtOperation.swift 3 | WWDC.srt 4 | 5 | Created by Seyed Samad Gholamzadeh on 7/19/1396 AP. 6 | Copyright © 1396 AP Seyed Samad Gholamzadeh. All rights reserved. 7 | 8 | Abstract: 9 | This file contains the code to download the m3u8 file. 10 | */ 11 | 12 | import Foundation 13 | 14 | /** 15 | An operation to download m3u8 file for input subtitle. 16 | - note: A m3u8 file, is a file blong to each video link and contains 17 | some informations about video subtitle files which are in webvtt format 18 | and we dwonload them later. 19 | 20 | */ 21 | final class DownloadM3U8Operation: GroupOperation { 22 | // MARK: Properties 23 | 24 | var subtitle: Subtitle 25 | 26 | /// - parameter subtitle: The subtitle which we want to download its m3u8 file. 27 | init(subtitle: Subtitle) { 28 | 29 | self.subtitle = subtitle 30 | 31 | super.init(operations: []) 32 | name = "Download M3U8" 33 | 34 | let task = URLSession.shared.downloadTask(with: subtitle.m3u8URL) { (url, response, error) in 35 | self.downloadFinished(url, response: response, error: error as NSError?) 36 | return() 37 | } 38 | 39 | let taskOperation = URLSessionTaskOperation(task: task) 40 | 41 | addOperation(taskOperation) 42 | 43 | } 44 | 45 | func downloadFinished(_ url: URL?, response: URLResponse?, error: NSError?) { 46 | if let localURL = url { 47 | 48 | do { 49 | 50 | /* 51 | We convert m3u8 file to an arry of `Webvtt` objects, 52 | which are the original subtitle files. 53 | We download and convert this webvtt files in a single srt file later. 54 | */ 55 | try self.subtitle.updateWebvtts(with: localURL) 56 | model.update(self.subtitle) 57 | } 58 | catch let error as NSError { 59 | aggregateError(error) 60 | } 61 | 62 | } 63 | else if let error = error { 64 | aggregateError(error) 65 | } 66 | else { 67 | // Do nothing, and the operation will automatically finish. 68 | } 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /WWDCSubGetter/DownloadWebvttOperation.swift: -------------------------------------------------------------------------------- 1 | /* 2 | DownloadWebvttOperation.swift 3 | WWDC.srt 4 | 5 | Created by Seyed Samad Gholamzadeh on 7/19/1396 AP. 6 | Copyright © 1396 AP Seyed Samad Gholamzadeh. All rights reserved. 7 | 8 | Abstract: 9 | This file contains the code to download the webvtt file. 10 | 11 | */ 12 | 13 | import Foundation 14 | 15 | /** 16 | An operation to download webvtt file. 17 | - note: Webvtt files, are bunch of files used for online videos subtitles. 18 | we download them and cnvert them to a single srt file. 19 | */ 20 | final class DownloadWebvttOperation: GroupOperation { 21 | 22 | var subtitle: Subtitle 23 | var webvtt: Webvtt 24 | 25 | init(subtitle: Subtitle, webvtt: Webvtt) { 26 | self.subtitle = subtitle 27 | self.webvtt = webvtt 28 | 29 | super.init(operations: []) 30 | name = "Download Webvtt \(webvtt.number)" 31 | 32 | let task = URLSession.shared.downloadTask(with: subtitle.url(for: webvtt)) { (url, response, error) in 33 | if let error = error { 34 | print(" the operation \(self.name!) of \(subtitle.videoURL) download task faced error : \(error)") 35 | } 36 | self.downloadFinished(url, response: response, error: error as NSError?) 37 | return() 38 | } 39 | 40 | let taskOperation = URLSessionTaskOperation(task: task) 41 | 42 | addOperation(taskOperation) 43 | 44 | } 45 | 46 | func downloadFinished(_ url: URL?, response: URLResponse?, error: NSError?) { 47 | if let localURL = url { 48 | 49 | do { 50 | /* 51 | After download finished, we get the content of webvtt file 52 | and convert it to a standard srt subtitle content. 53 | */ 54 | var content = try String(contentsOf: localURL, encoding: String.Encoding.utf8) 55 | content = content.replace("WEBVTT", with: "").replace(">", with: ">").replace("<", with: "<").replace("&", with: "&") 56 | 57 | let pattern = "\nX-TIMESTAMP-MAP[\\w+:,=.//d+]+\n\n" 58 | content = content.replace(matches: pattern, with: "")! 59 | 60 | let newContent = self.convertWebvttToSrt(webvtt: content) 61 | webvtt.content = newContent 62 | 63 | model.add(self.webvtt, to: self.subtitle) 64 | 65 | } 66 | catch let error as NSError { 67 | aggregateError(error) 68 | } 69 | 70 | } 71 | else if let error = error { 72 | aggregateError(error) 73 | } 74 | else { 75 | // Do nothing, and the operation will automatically finish. 76 | } 77 | 78 | } 79 | 80 | ///This method converts webvtt standard content to srt standard content 81 | func convertWebvttToSrt(webvtt: String) -> String { 82 | let timeLinePattern = "([0-9:.]+)\\.(\\d+ --> [0-9:.]+)\\.(\\d+) (\\w+:\\w+)\\%?" 83 | var firstLines:[String] = [] 84 | var newLines:[String] = [] 85 | var newSrt = webvtt 86 | 87 | firstLines = webvtt.list(matches: timeLinePattern) ?? [] 88 | 89 | if !firstLines.isEmpty { 90 | for line in firstLines { 91 | 92 | let groups = line.captureGroups(with: timeLinePattern)! 93 | 94 | let newLine: String = groups[1] + "," + groups[2] + "," + groups[3] 95 | newLines.append(newLine) 96 | } 97 | 98 | for i in 0...firstLines.count-1 { 99 | newSrt = newSrt.replace(firstLines[i], with: newLines[i]) 100 | } 101 | } 102 | return newSrt 103 | } 104 | 105 | 106 | } 107 | -------------------------------------------------------------------------------- /WWDCSubGetter/ExportLinksOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExportLinksOperation.swift 3 | // WWDC.srt 4 | // 5 | // Created by Seyed Samad Gholamzadeh on 6/7/18. 6 | // Copyright © 2018 Seyed Samad Gholamzadeh. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A subclass of Operation which executes `exportSrtFile()` method for input subtitle. 12 | final class ExportLinksOperation: Operation { 13 | 14 | let wwdcYear: WWDC 15 | let copyToUserDestinationURL: Bool 16 | 17 | /// - parameter subtitle: subtitle we want execute it's `exportSrtFile()` method. 18 | init(wwdcYear: WWDC, copyToUserDestinationURL: Bool) { 19 | self.wwdcYear = wwdcYear 20 | self.copyToUserDestinationURL = copyToUserDestinationURL 21 | 22 | super.init() 23 | name = "ExportLinksOperation" 24 | } 25 | 26 | 27 | override func execute() { 28 | 29 | let fileManager = FileManager.default 30 | 31 | //userDestinationURL 32 | let userDestinationURL = model.destinationURL! 33 | 34 | do { 35 | 36 | if copyToUserDestinationURL { 37 | if !fileManager.fileExists(atPath: userDestinationURL.path) { 38 | try fileManager.createDirectory(at: userDestinationURL, withIntermediateDirectories: true, attributes: nil) 39 | } 40 | } 41 | 42 | if !linksModel.titles.isEmpty, !copyToUserDestinationURL { 43 | let titlesURL = linksModel.titlesCacheURLFor(wwdcYear) 44 | let text = linksModel.titles.removingDuplicates().sorted().joined(separator: "\n") 45 | try text.write(to: titlesURL, atomically: false, encoding: String.Encoding.utf8) 46 | } 47 | 48 | if !linksModel.hdVideosLinks.isEmpty { 49 | let text = linksModel.hdVideosLinks.removingDuplicates().sorted().joined(separator: "\n") 50 | if copyToUserDestinationURL { 51 | let userHdVideoLinksURL = linksModel.userHdVideoLinksURLFor(wwdcYear) 52 | try text.write(to: userHdVideoLinksURL, atomically: false, encoding: String.Encoding.utf8) 53 | } 54 | else { 55 | let hdVideoLinksURL = linksModel.hdVideoCacheURLFor(wwdcYear) 56 | try text.write(to: hdVideoLinksURL, atomically: false, encoding: String.Encoding.utf8) 57 | } 58 | } 59 | 60 | if !linksModel.sdVideosLinks.isEmpty { 61 | let text = linksModel.sdVideosLinks.removingDuplicates().sorted().joined(separator: "\n") 62 | if copyToUserDestinationURL { 63 | let userSdVideoLinksURL = linksModel.userSdVideoLinksURLFor(wwdcYear) 64 | try text.write(to: userSdVideoLinksURL, atomically: false, encoding: String.Encoding.utf8) 65 | } 66 | else { 67 | let sdVideoLinksURL = linksModel.sdVideoCacheURLFor(wwdcYear) 68 | try text.write(to: sdVideoLinksURL, atomically: false, encoding: String.Encoding.utf8) 69 | } 70 | } 71 | 72 | if !linksModel.pdfLinks.isEmpty { 73 | let text = linksModel.pdfLinks.removingDuplicates().sorted().joined(separator: "\n") 74 | if copyToUserDestinationURL { 75 | let userPdfLinksURL = linksModel.userPdfLinksURLFor(wwdcYear) 76 | try text.write(to: userPdfLinksURL, atomically: false, encoding: String.Encoding.utf8) 77 | } 78 | else { 79 | let pdfLinksURL = linksModel.pdfLinksCacheURLFor(wwdcYear) 80 | try text.write(to: pdfLinksURL, atomically: false, encoding: String.Encoding.utf8) 81 | } 82 | } 83 | 84 | if !linksModel.sampleCodesLinks.isEmpty { 85 | let text = linksModel.sampleCodesLinks.removingDuplicates().sorted().joined(separator: "\n") 86 | if copyToUserDestinationURL { 87 | let userSampleCodesLinksURL = linksModel.userSampleCodesLinksURLFor(wwdcYear) 88 | try text.write(to: userSampleCodesLinksURL, atomically: false, encoding: String.Encoding.utf8) 89 | } 90 | else { 91 | let sampleCodesLinksURL = linksModel.sampleCodesLinksCacheURLFor(wwdcYear) 92 | try text.write(to: sampleCodesLinksURL, atomically: false, encoding: String.Encoding.utf8) 93 | 94 | } 95 | 96 | } 97 | 98 | } 99 | catch {/* error handling here */ 100 | print(error.localizedDescription) 101 | } 102 | 103 | linksModel.clear() 104 | finish() 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /WWDCSubGetter/ExportSrtFileOperation.swift: -------------------------------------------------------------------------------- 1 | /* 2 | ExportSrtFilesOperation.swift 3 | WWDC.srt 4 | 5 | Created by Seyed Samad Gholamzadeh on 7/20/1396 AP. 6 | Copyright © 1396 AP Seyed Samad Gholamzadeh. All rights reserved. 7 | 8 | Abstract: This files export srt files 9 | */ 10 | 11 | import Foundation 12 | 13 | /// A subclass of Operation which executes `exportSrtFile()` method for input subtitle. 14 | final class ExportSrtFileOperation: Operation { 15 | 16 | let subtitle: Subtitle 17 | 18 | /// - parameter subtitle: subtitle we want execute it's `exportSrtFile()` method. 19 | init(subtitle: Subtitle) { 20 | self.subtitle = subtitle 21 | super.init() 22 | name = "Export Srt File" 23 | } 24 | 25 | 26 | override func execute() { 27 | 28 | let subtitle = model.subtitle(for: self.subtitle.id)! 29 | 30 | subtitle.exportSrtFile() 31 | finish() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /WWDCSubGetter/FetchHtmlVideoPagesOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrseSessionsListOperation.swift 3 | // WWDC.srt 4 | // 5 | // Created by Seyed Samad Gholamzadeh on 6/6/18. 6 | // Copyright © 2018 Seyed Samad Gholamzadeh. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | class FetchHtmlVideoPagesOperation: GroupOperation { 13 | 14 | //MARK: Properties 15 | 16 | // let sessionListURL: URL 17 | let wwdcYear: WWDC 18 | let types: [SessionDataTypes] 19 | let sessionNumber: String? 20 | //MARK: Initializer 21 | 22 | init(for types: [SessionDataTypes], wwdcYear: WWDC, sessionNumber: String?) { 23 | 24 | self.wwdcYear = wwdcYear 25 | self.types = types 26 | self.sessionNumber = sessionNumber 27 | 28 | super.init(operations: []) 29 | self.name = "ParseSessionsListOperation" 30 | self.limitMaxConcurrentOperations(to: 3) 31 | } 32 | 33 | override func execute() { 34 | let cachesFolder = try! FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true) 35 | let downloadLinksCacheFolder = cachesFolder.appendingPathComponent("com.samad.WWDC.srt/\(wwdcYear.stringValue)/", isDirectory: true) 36 | 37 | var operations: [Foundation.Operation] = [] 38 | 39 | do { 40 | 41 | var sessionNumbers: [String] = [] 42 | 43 | if self.sessionNumber == nil { 44 | 45 | let titleURL = linksModel.titlesCacheURLFor(wwdcYear) 46 | 47 | if FileManager.default.fileExists(atPath: titleURL.path) { 48 | 49 | let data = try String(contentsOfFile:titleURL.path, encoding: String.Encoding.utf8) 50 | let sessionsListArray = data.components(separatedBy: "\n") 51 | sessionNumbers = sessionsListArray.compactMap { String($0.split(separator: " ").first!) } 52 | 53 | } 54 | 55 | } 56 | else { 57 | sessionNumbers = [self.sessionNumber!] 58 | } 59 | 60 | for sessionNumber in sessionNumbers { 61 | 62 | let htmlURL = downloadLinksCacheFolder.appendingPathComponent("\(sessionNumber).html") 63 | 64 | let parseHtmlOperation = ParseHtmlVideoPageOperation(for: types, sessionNumber: sessionNumber, cacheFile: htmlURL) 65 | 66 | if self.wwdcYear == lastWWDC { 67 | let getHtmlOperation = DownloadHtmlVideoPageOperation(wwdcYear: wwdcYear, sessionNumber: sessionNumber, cacheFile: htmlURL) 68 | operations.append(getHtmlOperation) 69 | 70 | 71 | parseHtmlOperation.addDependency(getHtmlOperation) 72 | } 73 | else if !FileManager.default.fileExists(atPath: htmlURL.path) { 74 | 75 | let getHtmlOperation = DownloadHtmlVideoPageOperation(wwdcYear: wwdcYear, sessionNumber: sessionNumber, cacheFile: htmlURL) 76 | operations.append(getHtmlOperation) 77 | 78 | 79 | parseHtmlOperation.addDependency(getHtmlOperation) 80 | } 81 | 82 | operations.append(parseHtmlOperation) 83 | } 84 | 85 | } catch { 86 | 87 | print(error) 88 | } 89 | 90 | addOperations(operations) 91 | 92 | super.execute() 93 | 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /WWDCSubGetter/FetchSubtitilesOperation.swift: -------------------------------------------------------------------------------- 1 | /* 2 | FetchSubtitilesOperation.swift 3 | WWDC.srt 4 | 5 | Created by Seyed Samad Gholamzadeh on 7/19/1396 AP. 6 | Copyright © 1396 AP Seyed Samad Gholamzadeh. All rights reserved. 7 | 8 | Abstract: 9 | In this file we download m3u8 file for each subtitle and fetch webvtt files 10 | and a th the end we export srt files. 11 | 12 | */ 13 | 14 | import Foundation 15 | 16 | /// A composite operation to download m3u8 files, fetching webvtt files and exporting srt file for each `Subtitle` concurrently. 17 | final class FetchSubtitilesOperation: GroupOperation { 18 | 19 | 20 | init() { 21 | super.init(operations: []) 22 | name = "FetchSubtitilesOp" 23 | 24 | // We limit maximum of concurrent operations to avoid of creating too many operations for a bunch of video links 25 | self.limitMaxConcurrentOperations(to: 3) 26 | 27 | for subtitle in model.allSubtitles() { 28 | 29 | let downloadOperation = DownloadM3U8Operation(subtitle: subtitle) 30 | let fetchWebvttsOperation = FetchWebvttsOperation(subtitle: subtitle) 31 | let exportSrtFileOperation = ExportSrtFileOperation(subtitle: subtitle) 32 | 33 | exportSrtFileOperation.addDependency(fetchWebvttsOperation) 34 | fetchWebvttsOperation.addDependency(downloadOperation) 35 | 36 | addOperations([downloadOperation, fetchWebvttsOperation, exportSrtFileOperation]) 37 | 38 | } 39 | 40 | 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /WWDCSubGetter/FetchWebvttsOperation.swift: -------------------------------------------------------------------------------- 1 | /* 2 | DownloadWebvttsOperation.swift 3 | WWDC.srt 4 | 5 | Created by Seyed Samad Gholamzadeh on 7/19/1396 AP. 6 | Copyright © 1396 AP Seyed Samad Gholamzadeh. All rights reserved. 7 | 8 | Abstract: 9 | This file fetches webvtt files related to each subtitle 10 | */ 11 | 12 | import Foundation 13 | 14 | ///A compose Operation to fetch webvtts files for input subtitle concurrently. 15 | final class FetchWebvttsOperation: GroupOperation { 16 | 17 | var subtitle: Subtitle 18 | 19 | /// - parameter subtitle: The subtitle which we want to fetch its webvtt files. 20 | init(subtitle: Subtitle) { 21 | self.subtitle = subtitle 22 | 23 | super.init(operations: []) 24 | name = "Fetch Webvtts" 25 | 26 | // We limit maximum of concurrent operations to avoid of creating too many operations for a bunch of Webvtt links 27 | self.limitMaxConcurrentOperations(to: 3) 28 | 29 | } 30 | 31 | /* 32 | We create child operations and add them to queue in `execute()` method instead of in `init(...)`. 33 | its because when we initialize this operation in `FetchSubtitlesOperation`, 34 | we don't have webvtts in subtitles `webvtts` array, so we must wait to 35 | download subtitle's m3u8 file and making webvtts, and then create webvtts 36 | download operations. 37 | */ 38 | 39 | override func execute() { 40 | 41 | /* 42 | Because Subtite is a struct and is a value type, we need to invoke it in `execute()` method 43 | to have an updated version of it 44 | */ 45 | let sub = model.subtitle(for: self.subtitle.id)! 46 | let webvttArray = sub.webvtts 47 | 48 | for webvtt in webvttArray { 49 | 50 | let downloadOperation = DownloadWebvttOperation(subtitle: sub, webvtt: webvtt) 51 | addOperation(downloadOperation) 52 | } 53 | 54 | // We clear subtitle webvtts array, because after downloading webvtts, we fill this array with new webvtts. 55 | model.clearWebvttArray(of: sub) 56 | 57 | super.execute() 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /WWDCSubGetter/GetHtmlSessionsListOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetHtmlSessionsListOperation.swift 3 | // WWDC.srt 4 | // 5 | // Created by Seyed Samad Gholamzadeh on 6/6/18. 6 | // Copyright © 2018 Seyed Samad Gholamzadeh. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | class GetHtmlSessionsListOperation: GroupOperation { 13 | 14 | //MARK: Properties 15 | 16 | let cacheFile: URL 17 | 18 | //MARK: Initializer 19 | 20 | /// -parameter cacheFile: The file `URL` to wich the earthquake feed will be downloaded. 21 | init(wwdcYear: WWDC, cacheFile: URL) { 22 | self.cacheFile = cacheFile 23 | super.init(operations: []) 24 | name = "GetHtmlSessionsListOperation" 25 | 26 | let url = URL(string: "https://developer.apple.com/videos/\(wwdcYear.stringValue)/")! 27 | 28 | let task = URLSession.shared.downloadTask(with: url) { (url, response, error) in 29 | self.downloadFinished(url, response: response, error: error as NSError?) 30 | return() 31 | } 32 | 33 | let taskOperation = URLSessionTaskOperation(task: task) 34 | 35 | let reachabilityCondition = ReachabilityCondition(host: url) 36 | taskOperation.addCondition(reachabilityCondition) 37 | 38 | addOperation(taskOperation) 39 | } 40 | 41 | func downloadFinished(_ url: URL?, response: URLResponse?, error: NSError?) { 42 | if let localURL = url { 43 | do { 44 | /* 45 | If we already have a file at this location, just delete it. 46 | Also swallow the error, because we don't really care about it. 47 | */ 48 | try FileManager.default.removeItem(at: cacheFile) 49 | } 50 | catch { } 51 | 52 | do { 53 | try FileManager.default.moveItem(at: localURL, to: cacheFile) 54 | } 55 | catch let error as NSError { 56 | aggregateError(error) 57 | } 58 | } 59 | else if let error = error { 60 | aggregateError(error) 61 | } 62 | else { 63 | // Do nothing, and the operation will automatically finish. 64 | } 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /WWDCSubGetter/GetLinksOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetLinksOperation.swift 3 | // WWDC.srt 4 | // 5 | // Created by Seyed Samad Gholamzadeh on 6/6/18. 6 | // Copyright © 2018 Seyed Samad Gholamzadeh. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | final class GetLinksOperation: GroupOperation { 12 | 13 | var wwdcYear: WWDC 14 | 15 | var fetchHtmlVideoPagesOperation: FetchHtmlVideoPagesOperation 16 | var exportLinksOperation: ExportLinksOperation! 17 | 18 | init(for types: [SessionDataTypes], wwdcYear: WWDC, sessionNumber: String? = nil, copyToUserDestinationURL: Bool, completionHandler: @escaping () -> Void) { 19 | self.wwdcYear = wwdcYear 20 | 21 | //com.samad.WWDC.srt 22 | 23 | self.fetchHtmlVideoPagesOperation = FetchHtmlVideoPagesOperation(for: types, wwdcYear: wwdcYear, sessionNumber: sessionNumber) 24 | 25 | self.exportLinksOperation = ExportLinksOperation(wwdcYear: wwdcYear, copyToUserDestinationURL: copyToUserDestinationURL) 26 | 27 | let finishOperation = Foundation.BlockOperation(block: completionHandler) 28 | 29 | self.exportLinksOperation.addDependency(self.fetchHtmlVideoPagesOperation) 30 | finishOperation.addDependency(self.exportLinksOperation!) 31 | 32 | 33 | let operations = [self.fetchHtmlVideoPagesOperation, self.exportLinksOperation, finishOperation].filter { $0 != nil } as! [Foundation.Operation] 34 | 35 | 36 | super.init(operations: operations) 37 | 38 | self.name = "GetLinksOperation" 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /WWDCSubGetter/GetSessionsListOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetLinksOperation.swift 3 | // WWDC.srt 4 | // 5 | // Created by Seyed Samad Gholamzadeh on 6/6/18. 6 | // Copyright © 2018 Seyed Samad Gholamzadeh. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | final class GetSessionsListOperation: GroupOperation { 12 | 13 | var wwdcYear: WWDC 14 | var getSessionsListOperation: GetHtmlSessionsListOperation? 15 | var parseSessionsListOperation: ParseSessionsListOperation 16 | 17 | init(for wwdcYear: WWDC, copyToUserDestinationURL: Bool, completionHandler: @escaping () -> Void) { 18 | self.wwdcYear = wwdcYear 19 | 20 | //com.samad.WWDC.srt 21 | 22 | let cachesFolder = try! FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true) 23 | 24 | let downloadLinksCacheFolder = cachesFolder.appendingPathComponent("com.samad.WWDC.srt/\(wwdcYear.stringValue)/", isDirectory: true) 25 | 26 | let fileManager = FileManager.default 27 | 28 | if !fileManager.fileExists(atPath: downloadLinksCacheFolder.path) { 29 | 30 | try! FileManager.default.createDirectory(atPath: downloadLinksCacheFolder.path, withIntermediateDirectories: true, attributes: nil) 31 | } 32 | 33 | let sessionListURL = downloadLinksCacheFolder.appendingPathComponent("sessionList.html") 34 | 35 | self.getSessionsListOperation = GetHtmlSessionsListOperation(wwdcYear: wwdcYear, cacheFile: sessionListURL) 36 | 37 | self.parseSessionsListOperation = ParseSessionsListOperation(for: wwdcYear, cacheFile: sessionListURL) 38 | 39 | 40 | let finishOperation = Foundation.BlockOperation(block: completionHandler) 41 | 42 | if self.getSessionsListOperation != nil { 43 | self.parseSessionsListOperation.addDependency(self.getSessionsListOperation!) 44 | } 45 | 46 | finishOperation.addDependency(self.parseSessionsListOperation) 47 | 48 | 49 | let operations = [self.getSessionsListOperation, self.parseSessionsListOperation, finishOperation].compactMap { $0 } 50 | 51 | super.init(operations: operations) 52 | 53 | self.name = "GetLinksOperation" 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /WWDCSubGetter/GetSubtitlesOperation.swift: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | GetSubtitleOperation.swift 4 | WWDC.srt 5 | 6 | Created by Seyed Samad Gholamzadeh on 7/19/1396 AP. 7 | Copyright © 1396 AP Seyed Samad Gholamzadeh. All rights reserved. 8 | 9 | Abstract: 10 | This file sets up the operations to fetch subtitles from internet. It will also decide to display an error message, if appropriate. 11 | */ 12 | 13 | import Cocoa 14 | 15 | /// A composite operation for fetching subtitles and clearing model. 16 | final class GetSubtitlesOperation: GroupOperation { 17 | 18 | /** 19 | - parameter completionHandler: The handler to call after fetching and 20 | clearing model are complete. This handler will be 21 | invoked on an arbitrary queue. 22 | */ 23 | init(completionHandler: @escaping () -> Void) { 24 | 25 | super.init(operations: []) 26 | name = "Get Subtitles" 27 | 28 | /* 29 | This operation is made of three child operations: 30 | 1. The operation to fetch subtitle. 31 | 2. The operation to clear model after fetching ends. 32 | 3. The operation to invoke the completion handler. 33 | */ 34 | 35 | let fetchSubtitlesOperation = FetchSubtitilesOperation() 36 | let sampleURL = URL(string: "https://www.google.com")! 37 | let reachabilityCondition = ReachabilityCondition(host: sampleURL) 38 | fetchSubtitlesOperation.addCondition(reachabilityCondition) 39 | 40 | let clearModelOperation = BlockOperation { (block) in 41 | model.clear() 42 | block() 43 | } 44 | 45 | let finishOperation = Foundation.BlockOperation(block: completionHandler) 46 | 47 | 48 | // These operations must be executed in order 49 | clearModelOperation.addDependency(fetchSubtitlesOperation) 50 | finishOperation.addDependency(fetchSubtitlesOperation) 51 | 52 | addOperations([fetchSubtitlesOperation, clearModelOperation, finishOperation]) 53 | } 54 | 55 | override func operationDidFinish(_ operation: Foundation.Operation, withErrors errors: [NSError]) { 56 | guard !errors.isEmpty else { return } 57 | self.produceAlert(errors.first!) 58 | } 59 | 60 | fileprivate func produceAlert(_ error: NSError) { 61 | /* 62 | Here we represent error in viewController. 63 | We only checks if the error is about reachability condition, 64 | if not it probably is about problem to find approperiate subtitle 65 | or failing to downlading it, by the way we alert user 66 | downlad of some subtitles failed. 67 | */ 68 | var errorDescription = "" 69 | if (error.userInfo[OperationConditionKey] as? String) == ReachabilityCondition.name { 70 | errorDescription = "Error: Can not Connect to internet. Please check your connection!" 71 | } 72 | else { 73 | errorDescription = "Error: Failed to download some subtitles." 74 | } 75 | if let presentationContext = NSApplication.shared.keyWindow?.contentViewController as? MainViewController { 76 | presentationContext.endDownloadingStatus(withError: errorDescription) 77 | } 78 | print(error) 79 | } 80 | 81 | 82 | } 83 | -------------------------------------------------------------------------------- /WWDCSubGetter/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleVersion 22 | 1 23 | LSApplicationCategoryType 24 | public.app-category.utilities 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | NSAppTransportSecurity 28 | 29 | NSAllowsArbitraryLoads 30 | 31 | 32 | NSHumanReadableCopyright 33 | Copyright © 2023 AP Seyed Samad Gholamzadeh. All rights reserved. 34 | NSMainStoryboardFile 35 | Main 36 | NSPrincipalClass 37 | NSApplication 38 | 39 | 40 | -------------------------------------------------------------------------------- /WWDCSubGetter/LinksModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LinksModel.swift 3 | // WWDC.srt 4 | // 5 | // Created by Seyed Samad Gholamzadeh on 6/6/18. 6 | // Copyright © 2018 Seyed Samad Gholamzadeh. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class SafeLinksModel { 12 | 13 | var queue = DispatchQueue(label: "LinkModel_Thread_Safe_Queue") 14 | 15 | private var _linksModel = LinksModel() 16 | 17 | 18 | var linksModel: LinksModel { 19 | get { 20 | queue.sync { 21 | return self._linksModel 22 | } 23 | } 24 | 25 | set { 26 | queue.async { 27 | self._linksModel = newValue 28 | } 29 | } 30 | } 31 | 32 | } 33 | 34 | fileprivate var safeLinksModel = SafeLinksModel() 35 | var linksModel: LinksModel { 36 | get { 37 | return safeLinksModel.linksModel 38 | } 39 | 40 | set { 41 | safeLinksModel.linksModel = newValue 42 | } 43 | } 44 | 45 | struct LinksModel { 46 | var titles: [String] = [] 47 | var hdVideosLinks: [String] = [] 48 | var sdVideosLinks: [String] = [] 49 | var pdfLinks: [String] = [] 50 | var sampleCodesLinks: [String] = [] 51 | let cachesFolder = try! FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true) 52 | 53 | 54 | mutating func clear() { 55 | self.hdVideosLinks.removeAll() 56 | self.sdVideosLinks.removeAll() 57 | self.pdfLinks.removeAll() 58 | self.sampleCodesLinks.removeAll() 59 | self.titles.removeAll() 60 | } 61 | 62 | var cacheDestinationURL: URL { 63 | let destinationURL = cachesFolder.appendingPathComponent("com.samad.WWDC.srt", isDirectory: true) 64 | return destinationURL 65 | } 66 | 67 | func cacheDestinationURFor(_ wwdcYear: WWDC) -> URL { 68 | let destinationURL = cachesFolder.appendingPathComponent("com.samad.WWDC.srt/\(wwdcYear.stringValue)/", isDirectory: true) 69 | return destinationURL 70 | } 71 | 72 | 73 | func titlesCacheURLFor(_ wwdcYear: WWDC) -> URL { 74 | let destinationURL = cacheDestinationURFor(wwdcYear) 75 | let titlesURL = destinationURL.appendingPathComponent("\(wwdcYear.stringValue)_sessions_titles.txt") 76 | 77 | return titlesURL 78 | } 79 | 80 | 81 | func hdVideoLinksFileNameFor(_ wwdcYear: WWDC) -> String { 82 | return "\(wwdcYear.stringValue)_hd_video_links.txt" 83 | } 84 | 85 | func hdVideoCacheURLFor(_ wwdcYear: WWDC) -> URL { 86 | let destinationURL = cacheDestinationURFor(wwdcYear) 87 | let titlesURL = destinationURL.appendingPathComponent(hdVideoLinksFileNameFor(wwdcYear)) 88 | 89 | return titlesURL 90 | } 91 | 92 | func userHdVideoLinksURLFor(_ wwdcYear: WWDC) -> URL { 93 | let userDestinationURL = model.destinationURL! 94 | let userHdVideoLinksURL = userDestinationURL.appendingPathComponent(hdVideoLinksFileNameFor(wwdcYear)) 95 | return userHdVideoLinksURL 96 | } 97 | 98 | func sdVideoLinksFileNameFor(_ wwdcYear: WWDC) -> String { 99 | return "\(wwdcYear.stringValue)_sd_video_links.txt" 100 | } 101 | 102 | func sdVideoCacheURLFor(_ wwdcYear: WWDC) -> URL { 103 | let destinationURL = cacheDestinationURFor(wwdcYear) 104 | let titlesURL = destinationURL.appendingPathComponent(sdVideoLinksFileNameFor(wwdcYear)) 105 | 106 | return titlesURL 107 | } 108 | 109 | func userSdVideoLinksURLFor(_ wwdcYear: WWDC) -> URL { 110 | let userDestinationURL = model.destinationURL! 111 | let userSdVideoLinksURL = userDestinationURL.appendingPathComponent(sdVideoLinksFileNameFor(wwdcYear)) 112 | return userSdVideoLinksURL 113 | } 114 | 115 | func pdfLinksFileNameFor(_ wwdcYear: WWDC) -> String { 116 | return "\(wwdcYear.stringValue)_pdf_links.txt" 117 | } 118 | 119 | func pdfLinksCacheURLFor(_ wwdcYear: WWDC) -> URL { 120 | let destinationURL = cacheDestinationURFor(wwdcYear) 121 | let titlesURL = destinationURL.appendingPathComponent(pdfLinksFileNameFor(wwdcYear)) 122 | 123 | return titlesURL 124 | } 125 | 126 | func userPdfLinksURLFor(_ wwdcYear: WWDC) -> URL { 127 | let userDestinationURL = model.destinationURL! 128 | let userPdfLinksURL = userDestinationURL.appendingPathComponent(pdfLinksFileNameFor(wwdcYear)) 129 | return userPdfLinksURL 130 | } 131 | 132 | func sampleCodesLinksFileNameFor(_ wwdcYear: WWDC) -> String { 133 | return "\(wwdcYear.stringValue)_sample_codes_links.txt" 134 | } 135 | 136 | func sampleCodesLinksCacheURLFor(_ wwdcYear: WWDC) -> URL { 137 | let destinationURL = cacheDestinationURFor(wwdcYear) 138 | let titlesURL = destinationURL.appendingPathComponent(sampleCodesLinksFileNameFor(wwdcYear)) 139 | 140 | return titlesURL 141 | } 142 | 143 | func userSampleCodesLinksURLFor(_ wwdcYear: WWDC) -> URL { 144 | let userDestinationURL = model.destinationURL! 145 | let userSampleCodesLinksURL = userDestinationURL.appendingPathComponent(sampleCodesLinksFileNameFor(wwdcYear)) 146 | return userSampleCodesLinksURL 147 | } 148 | 149 | 150 | 151 | } 152 | -------------------------------------------------------------------------------- /WWDCSubGetter/Model.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Model.swift 3 | WWDC.srt 4 | 5 | Created by Seyed Samad Gholamzadeh on 6/25/1396 AP. 6 | Copyright © 1396 AP Seyed Samad Gholamzadeh. All rights reserved. 7 | 8 | Abstract: 9 | This file contains struct Model which manages subtitles. 10 | */ 11 | 12 | import Foundation 13 | 14 | typealias ID = Int 15 | 16 | /** 17 | We created a global instance of `Model`, thus we can manage our subtitles easily 18 | in every class and struct. 19 | */ 20 | 21 | class SafeModel { 22 | 23 | var queue = DispatchQueue(label: "Model_Thread_Safe_Queue") 24 | 25 | private var _model = Model() 26 | 27 | 28 | var model: Model { 29 | get { 30 | queue.sync { 31 | return self._model 32 | } 33 | } 34 | 35 | set { 36 | queue.async { 37 | self._model = newValue 38 | } 39 | } 40 | } 41 | 42 | } 43 | 44 | fileprivate var safeModel = SafeModel() 45 | 46 | var model: Model { 47 | get { 48 | return safeModel.model 49 | } 50 | 51 | set { 52 | safeModel.model = newValue 53 | } 54 | } 55 | 56 | 57 | 58 | struct Model { 59 | 60 | /// A dictionay that saves subtitles with their id 61 | private var videosSub: [Int: Subtitle] = [:] 62 | 63 | /// The destination Local URL Which subtitles exported there 64 | var destinationURL: URL? 65 | 66 | /// A Boolean value that indicates whether the model is empty. 67 | var isEmpty: Bool { 68 | return self.videosSub.isEmpty 69 | } 70 | 71 | /** 72 | This method create a subtitle object with given `videoURL` 73 | and if it wasn't nil, adds it to the model. 74 | */ 75 | mutating func createSubtitle(with videoURL: String, wwdc: WWDC) { 76 | if let subtitle = Subtitle(videoURL: videoURL, wwdc: wwdc) { 77 | self.videosSub[subtitle.id] = subtitle 78 | } 79 | } 80 | 81 | /// This method updates model subtitles, with the given subtitles. 82 | mutating func update(_ subtitles: [Subtitle]) { 83 | for subtitle in subtitles { 84 | self.videosSub[subtitle.id] = subtitle 85 | } 86 | } 87 | 88 | /// This method updates model subtitle, with the given subtitle. 89 | mutating func update(_ subtitle: Subtitle) { 90 | self.update([subtitle]) 91 | } 92 | 93 | /// This method returns subtitle that saved in the model with the given id 94 | func subtitle(for id: ID) -> Subtitle? { 95 | return self.videosSub[id] 96 | } 97 | 98 | /// This method returns all subtitles saved in the model. 99 | func allSubtitles() -> [Subtitle] { 100 | return self.videosSub.values.sorted() 101 | } 102 | 103 | /// This method adds given webvtt to the specified model subtitle 104 | mutating func add(_ webvtt: Webvtt, to subtitle: Subtitle) { 105 | self.videosSub[subtitle.id]?.appendWebvtt(webvtt) 106 | } 107 | 108 | /// This method clears webvttArray of the specified model subtitle 109 | mutating func clearWebvttArray(of subtitle: Subtitle) { 110 | self.videosSub[subtitle.id]?.clearWebvtts() 111 | } 112 | 113 | /// This method clears all subtitles saved in the model. 114 | mutating func clear() { 115 | self.videosSub.removeAll() 116 | } 117 | 118 | } 119 | 120 | 121 | -------------------------------------------------------------------------------- /WWDCSubGetter/Operations/Conditions/MutuallyExclusive.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2015 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file shows an example of implementing the OperationCondition protocol. 7 | */ 8 | 9 | import Foundation 10 | 11 | /// A generic condition for describing kinds of operations that may not execute concurrently. 12 | struct MutuallyExclusive: OperationCondition { 13 | 14 | 15 | static var name: String { 16 | return "MutuallyExclusive<\(T.self)>" 17 | } 18 | 19 | static var isMutuallyExclusive: Bool { 20 | return true 21 | } 22 | 23 | init() { } 24 | 25 | func dependencyForOperation(_ operation: Operation) -> Foundation.Operation? { 26 | return nil 27 | } 28 | 29 | func evaluateForOperation(_ operation: Operation, completion: @escaping (OperationConditionResult) -> Void) { 30 | completion(.satisfied) 31 | } 32 | } 33 | 34 | /** 35 | The purpose of this enum is to simply provide a non-constructible 36 | type to be used with `MutuallyExclusive`. 37 | */ 38 | enum Alert { } 39 | 40 | /// A condition describing that the targeted operation may present an alert. 41 | typealias AlertPresentation = MutuallyExclusive 42 | 43 | -------------------------------------------------------------------------------- /WWDCSubGetter/Operations/Conditions/OperationCondition.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2015 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file contains the fundamental logic relating to Operation conditions. 7 | */ 8 | 9 | import Foundation 10 | 11 | let OperationConditionKey = "OperationCondition" 12 | 13 | /** 14 | A protocol for defining conditions that must be satisfied in order for an 15 | operation to begin execution. 16 | */ 17 | protocol OperationCondition { 18 | /** 19 | The name of the condition. This is used in userInfo dictionaries of `.ConditionFailed` 20 | errors as the value of the `OperationConditionKey` key. 21 | */ 22 | static var name: String { get } 23 | 24 | /** 25 | Specifies whether multiple instances of the conditionalized operation may 26 | be executing simultaneously. 27 | */ 28 | static var isMutuallyExclusive: Bool { get } 29 | 30 | /** 31 | Some conditions may have the ability to satisfy the condition if another 32 | operation is executed first. Use this method to return an operation that 33 | (for example) asks for permission to perform the operation 34 | 35 | - parameter operation: The `Operation` to which the Condition has been added. 36 | - returns: An `NSOperation`, if a dependency should be automatically added. Otherwise, `nil`. 37 | - note: Only a single operation may be returned as a dependency. If you 38 | find that you need to return multiple operations, then you should be 39 | expressing that as multiple conditions. Alternatively, you could return 40 | a single `GroupOperation` that executes multiple operations internally. 41 | */ 42 | func dependencyForOperation(_ operation: Operation) -> Foundation.Operation? 43 | 44 | /// Evaluate the condition, to see if it has been satisfied or not. 45 | func evaluateForOperation(_ operation: Operation, completion: @escaping (OperationConditionResult) -> Void) 46 | } 47 | 48 | /** 49 | An enum to indicate whether an `OperationCondition` was satisfied, or if it 50 | failed with an error. 51 | */ 52 | enum OperationConditionResult: Equatable { 53 | case satisfied 54 | case failed(NSError) 55 | 56 | var error: NSError? { 57 | if case .failed(let error) = self { 58 | return error 59 | } 60 | 61 | return nil 62 | } 63 | } 64 | 65 | func ==(lhs: OperationConditionResult, rhs: OperationConditionResult) -> Bool { 66 | switch (lhs, rhs) { 67 | case (.satisfied, .satisfied): 68 | return true 69 | case (.failed(let lError), .failed(let rError)) where lError == rError: 70 | return true 71 | default: 72 | return false 73 | } 74 | } 75 | 76 | // MARK: Evaluate Conditions 77 | 78 | struct OperationConditionEvaluator { 79 | static func evaluate(_ conditions: [OperationCondition], operation: Operation, completion: @escaping ([NSError]) -> Void) { 80 | // Check conditions. 81 | let conditionGroup = DispatchGroup() 82 | 83 | var results = [OperationConditionResult?](repeating: nil, count: conditions.count) 84 | 85 | // Ask each condition to evaluate and store its result in the "results" array. 86 | for (index, condition) in conditions.enumerated() { 87 | conditionGroup.enter() 88 | condition.evaluateForOperation(operation) { result in 89 | results[index] = result 90 | conditionGroup.leave() 91 | } 92 | } 93 | 94 | // After all the conditions have evaluated, this block will execute. 95 | conditionGroup.notify(queue: DispatchQueue.global(qos: DispatchQoS.QoSClass.default)) { 96 | // Aggregate the errors that occurred, in order. 97 | var failures = results.compactMap { $0?.error } 98 | 99 | /* 100 | If any of the conditions caused this operation to be cancelled, 101 | check for that. 102 | */ 103 | if operation.isCancelled { 104 | failures.append(NSError(code: .conditionFailed)) 105 | } 106 | 107 | completion(failures) 108 | } 109 | } 110 | } 111 | 112 | -------------------------------------------------------------------------------- /WWDCSubGetter/Operations/Conditions/OperationErrors.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2015 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file defines the error codes and convenience functions for interacting with Operation-related errors. 7 | */ 8 | 9 | import Foundation 10 | 11 | let OperationErrorDomain = "OperationErrors" 12 | 13 | enum OperationErrorCode: Int { 14 | case conditionFailed = 1 15 | case executionFailed = 2 16 | } 17 | 18 | extension NSError { 19 | convenience init(code: OperationErrorCode, userInfo: [String: Any]? = nil) { 20 | self.init(domain: OperationErrorDomain, code: code.rawValue, userInfo: userInfo) 21 | } 22 | } 23 | 24 | // This makes it easy to compare an `NSError.code` to an `OperationErrorCode`. 25 | func ==(lhs: Int, rhs: OperationErrorCode) -> Bool { 26 | return lhs == rhs.rawValue 27 | } 28 | 29 | func ==(lhs: OperationErrorCode, rhs: Int) -> Bool { 30 | return lhs.rawValue == rhs 31 | } 32 | 33 | -------------------------------------------------------------------------------- /WWDCSubGetter/Operations/Conditions/ReachabilityCondition.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2015 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file shows an example of implementing the OperationCondition protocol. 7 | */ 8 | 9 | import Foundation 10 | import SystemConfiguration 11 | 12 | /** 13 | This is a condition that performs a very high-level reachability check. 14 | It does *not* perform a long-running reachability check, nor does it respond to changes in reachability. 15 | Reachability is evaluated once when the operation to which this is attached is asked about its readiness. 16 | */ 17 | struct ReachabilityCondition: OperationCondition { 18 | static let hostKey = "Host" 19 | static let name = "Reachability" 20 | static let isMutuallyExclusive = false 21 | 22 | let host: URL 23 | 24 | init(host: URL) { 25 | self.host = host 26 | } 27 | 28 | func dependencyForOperation(_ operation: Operation) -> Foundation.Operation? { 29 | return nil 30 | } 31 | 32 | func evaluateForOperation(_ operation: Operation, completion: @escaping (OperationConditionResult) -> Void) { 33 | ReachabilityController.requestReachability(host) { reachable in 34 | if reachable { 35 | completion(.satisfied) 36 | } 37 | else { 38 | let error = NSError(code: .conditionFailed, userInfo: [ 39 | OperationConditionKey: type(of: self).name, 40 | type(of: self).hostKey: self.host 41 | ]) 42 | 43 | completion(.failed(error)) 44 | } 45 | } 46 | } 47 | 48 | } 49 | 50 | /// A private singleton that maintains a basic cache of `SCNetworkReachability` objects. 51 | private class ReachabilityController { 52 | static var reachabilityRefs = [String: SCNetworkReachability]() 53 | 54 | static let reachabilityQueue = DispatchQueue(label: "Operations.Reachability", attributes: []) 55 | 56 | static func requestReachability(_ url: URL, completionHandler: @escaping (Bool) -> Void) { 57 | if let host = url.host { 58 | reachabilityQueue.async { 59 | var ref = self.reachabilityRefs[host] 60 | 61 | if ref == nil { 62 | let hostString = host as NSString 63 | ref = SCNetworkReachabilityCreateWithName(nil, hostString.utf8String!) 64 | } 65 | 66 | if let ref = ref { 67 | self.reachabilityRefs[host] = ref 68 | 69 | var reachable = false 70 | var flags: SCNetworkReachabilityFlags = [] 71 | if SCNetworkReachabilityGetFlags(ref, &flags) != false { 72 | /* 73 | Note that this is a very basic "is reachable" check. 74 | Your app may choose to allow for other considerations, 75 | such as whether or not the connection would require 76 | VPN, a cellular connection, etc. 77 | */ 78 | reachable = flags.contains(.reachable) 79 | } 80 | completionHandler(reachable) 81 | } 82 | else { 83 | completionHandler(false) 84 | } 85 | } 86 | } 87 | else { 88 | completionHandler(false) 89 | } 90 | } 91 | } 92 | 93 | -------------------------------------------------------------------------------- /WWDCSubGetter/Operations/Foundation.Operation+Operation.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2015 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | A convenient extension to Foundation.NSOperation. 7 | */ 8 | 9 | import Foundation 10 | 11 | extension Foundation.Operation { 12 | /** 13 | Add a completion block to be executed after the `NSOperation` enters the 14 | "finished" state. 15 | */ 16 | func addCompletionBlock(_ block: @escaping () -> Void) { 17 | if let existing = completionBlock { 18 | /* 19 | If we already have a completion block, we construct a new one by 20 | chaining them together. 21 | */ 22 | completionBlock = { 23 | existing() 24 | block() 25 | } 26 | } 27 | else { 28 | completionBlock = block 29 | } 30 | } 31 | 32 | /// Add multiple depdendencies to the operation. 33 | func addDependencies(_ dependencies: [Foundation.Operation]) { 34 | for dependency in dependencies { 35 | addDependency(dependency) 36 | } 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /WWDCSubGetter/Operations/NSLock+Operations.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2015 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | An extension to NSLock to simplify executing critical code. 7 | */ 8 | 9 | import Foundation 10 | 11 | extension NSLock { 12 | func withCriticalScope(_ block: () -> T) -> T { 13 | lock() 14 | let value = block() 15 | unlock() 16 | return value 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /WWDCSubGetter/Operations/Observers/BlockObserver.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2015 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file shows how to implement the OperationObserver protocol. 7 | */ 8 | 9 | import Foundation 10 | 11 | /** 12 | The `BlockObserver` is a way to attach arbitrary blocks to significant events 13 | in an `Operation`'s lifecycle. 14 | */ 15 | struct BlockObserver: OperationObserver { 16 | // MARK: Properties 17 | 18 | fileprivate let startHandler: ((Operation) -> Void)? 19 | fileprivate let produceHandler: ((Operation, Foundation.Operation) -> Void)? 20 | fileprivate let finishHandler: ((Operation, [NSError]) -> Void)? 21 | 22 | init(startHandler: ((Operation) -> Void)? = nil, produceHandler: ((Operation, Foundation.Operation) -> Void)? = nil, finishHandler: ((Operation, [NSError]) -> Void)? = nil) { 23 | self.startHandler = startHandler 24 | self.produceHandler = produceHandler 25 | self.finishHandler = finishHandler 26 | } 27 | 28 | // MARK: OperationObserver 29 | 30 | func operationDidStart(_ operation: Operation) { 31 | startHandler?(operation) 32 | } 33 | 34 | func operation(_ operation: Operation, didProduceOperation newOperation: Foundation.Operation) { 35 | produceHandler?(operation, newOperation) 36 | } 37 | 38 | func operationDidFinish(_ operation: Operation, errors: [NSError]) { 39 | finishHandler?(operation, errors) 40 | } 41 | } 42 | 43 | -------------------------------------------------------------------------------- /WWDCSubGetter/Operations/Observers/NetworkObserver.swift: -------------------------------------------------------------------------------- 1 | /* 2 | NetworkObserver.swift 3 | MyOperationPractice 4 | 5 | Created by Seyed Samad Gholamzadeh on 7/14/1396 AP. 6 | Copyright © 1396 AP Seyed Samad Gholamzadeh. All rights reserved. 7 | 8 | Abstract: 9 | Contains the code to manage the visibility of the network activity indicator 10 | */ 11 | 12 | import UIKit 13 | 14 | /** 15 | An `OperationObserver` that will cause the network activity indicatior to appear as long 16 | as the `Operation` to which it is attached is executing. 17 | */ 18 | 19 | struct NetworkObserver: OperationObserver { 20 | //MARk: Initialization 21 | 22 | init() { } 23 | 24 | func operationDidStart(_ operation: Operation) { 25 | DispatchQueue.main.async { 26 | // Increment the network indicator's "reference count" 27 | NetworkIndicatorController.shared.networkActivityDidStart() 28 | } 29 | } 30 | 31 | func operation(_ operation: Operation, didProduceOperation newOperation: Foundation.Operation) { } 32 | 33 | func operationDidFinish(_ operation: Operation, errors: [NSError]) { 34 | DispatchQueue.main.async { 35 | // Decrement the network indicator's "reference count". 36 | NetworkIndicatorController.shared.networkActivityDidEnd() 37 | } 38 | } 39 | } 40 | 41 | 42 | /// A singleton to manage a visual "reference count" on the network activity indicator. 43 | private class NetworkIndicatorController { 44 | // MARK: Properties 45 | 46 | static let shared = NetworkIndicatorController() 47 | 48 | fileprivate var activityCount = 0 49 | 50 | fileprivate var visibilityTimer: Timer? 51 | 52 | // MARK: Methods 53 | 54 | func networkActivityDidStart() { 55 | assert(Thread.isMainThread, "Alerting network activity indicator state can only be done on the main thread.") 56 | 57 | activityCount += 1 58 | 59 | updateIndicatorVisibility() 60 | } 61 | 62 | func networkActivityDidEnd() { 63 | assert(Thread.isMainThread, "Alerting network activity indicator state can only be done on the main thread.") 64 | 65 | activityCount -= 1 66 | 67 | updateIndicatorVisibility() 68 | } 69 | 70 | func updateIndicatorVisibility() { 71 | if activityCount > 0 { 72 | showIndicator() 73 | } 74 | else { 75 | /* 76 | To perevent the indicator from flickering on and off, we delay the hiding 77 | of the indicator by one second. This provides the chance to come in and 78 | invalidate the timer before it fires. 79 | */ 80 | visibilityTimer = Timer(interval: 1.0) { 81 | self.hideIndicator() 82 | } 83 | } 84 | } 85 | 86 | fileprivate func showIndicator() { 87 | visibilityTimer?.cancel() 88 | visibilityTimer = nil 89 | UIApplication.shared.isNetworkActivityIndicatorVisible = true 90 | } 91 | 92 | fileprivate func hideIndicator() { 93 | visibilityTimer?.cancel() 94 | visibilityTimer = nil 95 | UIApplication.shared.isNetworkActivityIndicatorVisible = false 96 | } 97 | } 98 | 99 | 100 | /// Essentially a cancelable `dispatch_after`. 101 | class Timer { 102 | //MARK: Properties 103 | 104 | fileprivate var isCancelled = false 105 | 106 | //MARK: Initialiazation 107 | 108 | init(interval: TimeInterval, handler: @escaping () -> Void) { 109 | let when = DispatchTime.now() + Double(Int64(interval * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC) 110 | 111 | DispatchQueue.main.asyncAfter(deadline: when) { [weak self] in 112 | if self?.isCancelled == false { 113 | handler() 114 | } 115 | } 116 | } 117 | 118 | func cancel() { 119 | isCancelled = true 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /WWDCSubGetter/Operations/Observers/OperationObserver.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2015 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file defines the OperationObserver protocol. 7 | */ 8 | 9 | import Foundation 10 | 11 | /** 12 | The protocol that types may implement if they wish to be notified of significant 13 | operation lifecycle events. 14 | */ 15 | protocol OperationObserver { 16 | 17 | /// Invoked immediately prior to the `Operation`'s `execute()` method. 18 | func operationDidStart(_ operation: Operation) 19 | 20 | /// Invoked when `Operation.produceOperation(_:)` is executed. 21 | func operation(_ operation: Operation, didProduceOperation newOperation: Foundation.Operation) 22 | 23 | /** 24 | Invoked as an `Operation` finishes, along with any errors produced during 25 | execution (or readiness evaluation). 26 | */ 27 | func operationDidFinish(_ operation: Operation, errors: [NSError]) 28 | 29 | } 30 | 31 | -------------------------------------------------------------------------------- /WWDCSubGetter/Operations/OperationQueue/ExclusivityController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2015 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file contains the code to automatically set up dependencies between mutually exclusive operations. 7 | */ 8 | 9 | import Foundation 10 | 11 | /** 12 | `ExclusivityController` is a singleton to keep track of all the in-flight 13 | `Operation` instances that have declared themselves as requiring mutual exclusivity. 14 | We use a singleton because mutual exclusivity must be enforced across the entire 15 | app, regardless of the `OperationQueue` on which an `Operation` was executed. 16 | */ 17 | class ExclusivityController { 18 | static let shared = ExclusivityController() 19 | 20 | fileprivate let serialQueue = DispatchQueue(label: "Operations.ExclusivityController", attributes: []) 21 | fileprivate var operations: [String: [Operation]] = [:] 22 | 23 | fileprivate init() { 24 | /* 25 | A private initializer effectively prevents any other part of the app 26 | from accidentally creating an instance. 27 | */ 28 | } 29 | 30 | /// Registers an operation as being mutually exclusive 31 | func addOperation(_ operation: Operation, categories: [String]) { 32 | /* 33 | This needs to be a synchronous operation. 34 | If this were async, then we might not get around to adding dependencies 35 | until after the operation had already begun, which would be incorrect. 36 | */ 37 | serialQueue.sync { 38 | for category in categories { 39 | self.noqueue_addOperation(operation, category: category) 40 | } 41 | } 42 | } 43 | 44 | /// Unregisters an operation from being mutually exclusive. 45 | func removeOperation(_ operation: Operation, categories: [String]) { 46 | serialQueue.async { 47 | for category in categories { 48 | self.noqueue_removeOperation(operation, category: category) 49 | } 50 | } 51 | } 52 | 53 | 54 | // MARK: Operation Management 55 | 56 | fileprivate func noqueue_addOperation(_ operation: Operation, category: String) { 57 | var operationsWithThisCategory = operations[category] ?? [] 58 | 59 | if let last = operationsWithThisCategory.last { 60 | operation.addDependency(last) 61 | } 62 | 63 | operationsWithThisCategory.append(operation) 64 | 65 | operations[category] = operationsWithThisCategory 66 | } 67 | 68 | fileprivate func noqueue_removeOperation(_ operation: Operation, category: String) { 69 | let matchingOperations = operations[category] 70 | 71 | if var operationsWithThisCategory = matchingOperations, 72 | let index = operationsWithThisCategory.firstIndex(of: operation) { 73 | 74 | operationsWithThisCategory.remove(at: index) 75 | operations[category] = operationsWithThisCategory 76 | } 77 | } 78 | 79 | } 80 | 81 | -------------------------------------------------------------------------------- /WWDCSubGetter/Operations/OperationQueue/OperationQueue.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2015 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file contains an NSOperationQueue subclass. 7 | */ 8 | 9 | import Foundation 10 | 11 | /** 12 | The delegate of an `OperationQueue` can respond to `Operation` lifecycle 13 | events by implementing these methods. 14 | 15 | In general, implementing `OperationQueueDelegate` is not necessary; you would 16 | want to use an `OperationObserver` instead. However, there are a couple of 17 | situations where using `OperationQueueDelegate` can lead to simpler code. 18 | For example, `GroupOperation` is the delegate of its own internal 19 | `OperationQueue` and uses it to manage dependencies. 20 | */ 21 | @objc protocol OperationQueueDelegate: NSObjectProtocol { 22 | @objc optional func operationQueue(_ operationQueue: OperationQueue, willAddOperation operation: Foundation.Operation) 23 | @objc optional func operationQueue(_ operationQueue: OperationQueue, operationDidFinish operation: Foundation.Operation, withErrors errors: [NSError]) 24 | } 25 | 26 | /** 27 | `OperationQueue` is an `NSOperationQueue` subclass that implements a large 28 | number of "extra features" related to the `Operation` class: 29 | 30 | - Notifying a delegate of all operation completion 31 | - Extracting generated dependencies from operation conditions 32 | - Setting up dependencies to enforce mutual exclusivity 33 | */ 34 | class OperationQueue: Foundation.OperationQueue { 35 | weak var delegate: OperationQueueDelegate? 36 | 37 | override func addOperation(_ op: Foundation.Operation) { 38 | if let op = op as? Operation { 39 | // Set up a `BlockObserver` to invoke the `OperationQueueDelegate` method. 40 | let delegate = BlockObserver( 41 | startHandler: nil, 42 | produceHandler: { [weak self] in 43 | self?.addOperation($1) 44 | }, 45 | finishHandler: { [weak self] in 46 | if let q = self { 47 | q.delegate?.operationQueue?(q, operationDidFinish: $0, withErrors: $1) 48 | } 49 | } 50 | ) 51 | op.addObserver(delegate) 52 | 53 | // Extract any dependencies needed by this operation. 54 | let dependencies = op.conditions.compactMap { 55 | $0.dependencyForOperation(op) 56 | } 57 | 58 | for dependency in dependencies { 59 | op.addDependency(dependency) 60 | 61 | self.addOperation(dependency) 62 | } 63 | 64 | /* 65 | With condition dependencies added, we can now see if this needs 66 | dependencies to enforce mutual exclusivity. 67 | */ 68 | let concurrencyCategories: [String] = op.conditions.compactMap { condition in 69 | if !type(of: condition).isMutuallyExclusive { return nil } 70 | 71 | return "\(type(of: condition))" 72 | } 73 | 74 | if !concurrencyCategories.isEmpty { 75 | // Set up the mutual exclusivity dependencies. 76 | let exclusivityController = ExclusivityController.shared 77 | 78 | exclusivityController.addOperation(op, categories: concurrencyCategories) 79 | 80 | op.addObserver(BlockObserver(finishHandler: { operation, _ in 81 | exclusivityController.removeOperation(operation, categories: concurrencyCategories) 82 | })) 83 | } 84 | 85 | /* 86 | Indicate to the operation that we've finished our extra work on it 87 | and it's now it a state where it can proceed with evaluating conditions, 88 | if appropriate. 89 | */ 90 | op.willEnqueue() 91 | } 92 | else { 93 | /* 94 | For regular `NSOperation`s, we'll manually call out to the queue's 95 | delegate we don't want to just capture "operation" because that 96 | would lead to the operation strongly referencing itself and that's 97 | the pure definition of a memory leak. 98 | */ 99 | op.addCompletionBlock { [weak self, weak op] in 100 | guard let queue = self, let operation = op else { return } 101 | queue.delegate?.operationQueue?(queue, operationDidFinish: operation, withErrors: []) 102 | } 103 | } 104 | 105 | delegate?.operationQueue?(self, willAddOperation: op) 106 | 107 | super.addOperation(op) 108 | } 109 | 110 | override func addOperations(_ ops: [Foundation.Operation], waitUntilFinished wait: Bool) { 111 | /* 112 | The base implementation of this method does not call `addOperation()`, 113 | so we'll call it ourselves. 114 | */ 115 | for operation in ops { 116 | addOperation(operation) 117 | } 118 | 119 | if wait { 120 | for operation in ops { 121 | operation.waitUntilFinished() 122 | } 123 | } 124 | } 125 | } 126 | 127 | -------------------------------------------------------------------------------- /WWDCSubGetter/Operations/Operations/AlertOperation.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2015 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file shows how to present an alert as part of an operation. 7 | */ 8 | 9 | import UIKit 10 | 11 | class AlertOperation: Operation { 12 | // MARK: Properties 13 | 14 | fileprivate let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .alert) 15 | fileprivate let presentationContext: UIViewController? 16 | 17 | var title: String? { 18 | get { 19 | return alertController.title 20 | } 21 | 22 | set { 23 | alertController.title = newValue 24 | name = newValue 25 | } 26 | } 27 | 28 | var message: String? { 29 | get { 30 | return alertController.message 31 | } 32 | 33 | set { 34 | alertController.message = newValue 35 | } 36 | } 37 | 38 | // MARK: Initialization 39 | 40 | init(presentationContext: UIViewController? = nil) { 41 | self.presentationContext = presentationContext ?? UIApplication.shared.keyWindow?.rootViewController 42 | 43 | super.init() 44 | 45 | addCondition(AlertPresentation()) 46 | 47 | /* 48 | This operation modifies the view controller hierarchy. 49 | Doing this while other such operations are executing can lead to 50 | inconsistencies in UIKit. So, let's make them mutally exclusive. 51 | */ 52 | addCondition(MutuallyExclusive()) 53 | } 54 | 55 | func addAction(_ title: String, style: UIAlertActionStyle = .default, handler: @escaping (AlertOperation) -> Void = { _ in }) { 56 | let action = UIAlertAction(title: title, style: style) { [weak self] _ in 57 | if let strongSelf = self { 58 | handler(strongSelf) 59 | } 60 | 61 | self?.finish() 62 | } 63 | 64 | alertController.addAction(action) 65 | } 66 | 67 | override func execute() { 68 | guard let presentationContext = presentationContext else { 69 | finish() 70 | 71 | return 72 | } 73 | 74 | DispatchQueue.main.async { 75 | if self.alertController.actions.isEmpty { 76 | self.addAction("OK") 77 | } 78 | 79 | presentationContext.present(self.alertController, animated: true, completion: nil) 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /WWDCSubGetter/Operations/Operations/BlockOperation.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2015 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This code shows how to create a simple subclass of Operation. 7 | */ 8 | 9 | import Foundation 10 | 11 | /// A closure type that takes a closure as its parameter. 12 | typealias OperationBlock = (@escaping () -> Void) -> Void 13 | 14 | /// A sublcass of `Operation` to execute a closure. 15 | class BlockOperation: Operation { 16 | fileprivate let block: OperationBlock? 17 | 18 | /** 19 | The designated initializer. 20 | 21 | - parameter block: The closure to run when the operation executes. This 22 | closure will be run on an arbitrary queue. The parameter passed to the 23 | block **MUST** be invoked by your code, or else the `BlockOperation` 24 | will never finish executing. If this parameter is `nil`, the operation 25 | will immediately finish. 26 | */ 27 | init(block: OperationBlock? = nil) { 28 | self.block = block 29 | super.init() 30 | } 31 | 32 | /** 33 | A convenience initializer to execute a block on the main queue. 34 | 35 | - parameter mainQueueBlock: The block to execute on the main queue. Note 36 | that this block does not have a "continuation" block to execute (unlike 37 | the designated initializer). The operation will be automatically ended 38 | after the `mainQueueBlock` is executed. 39 | */ 40 | convenience init(mainQueueBlock: @escaping ()->()) { 41 | self.init(block: { continuation in 42 | DispatchQueue.main.async { 43 | mainQueueBlock() 44 | continuation() 45 | } 46 | }) 47 | } 48 | 49 | override func execute() { 50 | guard let block = block else { 51 | finish() 52 | return 53 | } 54 | 55 | block { 56 | self.finish() 57 | } 58 | } 59 | } 60 | 61 | -------------------------------------------------------------------------------- /WWDCSubGetter/Operations/Operations/GroupOperation.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2015 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file shows how operations can be composed together to form new operations. 7 | */ 8 | 9 | import Foundation 10 | 11 | /** 12 | A subclass of `Operation` that executes zero or more operations as part of its 13 | own execution. This class of operation is very useful for abstracting several 14 | smaller operations into a larger operation. As an example, the `GetEarthquakesOperation` 15 | is composed of both a `DownloadEarthquakesOperation` and a `ParseEarthquakesOperation`. 16 | 17 | Additionally, `GroupOperation`s are useful if you establish a chain of dependencies, 18 | but part of the chain may "loop". For example, if you have an operation that 19 | requires the user to be authenticated, you may consider putting the "login" 20 | operation inside a group operation. That way, the "login" operation may produce 21 | subsequent operations (still within the outer `GroupOperation`) that will all 22 | be executed before the rest of the operations in the initial chain of operations. 23 | */ 24 | class GroupOperation: Operation { 25 | fileprivate let internalQueue = OperationQueue() 26 | fileprivate let startingOperation = Foundation.BlockOperation(block: {}) 27 | fileprivate let finishingOperation = Foundation.BlockOperation(block: {}) 28 | 29 | fileprivate var aggregatedErrors = [NSError]() 30 | fileprivate let serialQueue = DispatchQueue(label: "GroupOperation_Serial_Queue") 31 | 32 | convenience init(operations: Foundation.Operation...) { 33 | self.init(operations: operations) 34 | } 35 | 36 | init(operations: [Foundation.Operation]) { 37 | super.init() 38 | 39 | internalQueue.isSuspended = true 40 | internalQueue.delegate = self 41 | internalQueue.addOperation(startingOperation) 42 | 43 | for operation in operations { 44 | internalQueue.addOperation(operation) 45 | } 46 | } 47 | 48 | func limitMaxConcurrentOperations(to maxCount: Int) { 49 | self.internalQueue.maxConcurrentOperationCount = maxCount 50 | } 51 | 52 | override func cancel() { 53 | internalQueue.cancelAllOperations() 54 | super.cancel() 55 | } 56 | 57 | override func execute() { 58 | internalQueue.isSuspended = false 59 | internalQueue.addOperation(finishingOperation) 60 | } 61 | 62 | func addOperation(_ operation: Foundation.Operation) { 63 | internalQueue.addOperation(operation) 64 | } 65 | 66 | func addOperations(_ operations: [Foundation.Operation]) { 67 | for operation in operations { 68 | internalQueue.addOperation(operation) 69 | } 70 | } 71 | 72 | 73 | /** 74 | Note that some part of execution has produced an error. 75 | Errors aggregated through this method will be included in the final array 76 | of errors reported to observers and to the `finished(_:)` method. 77 | */ 78 | final func aggregateError(_ error: NSError) { 79 | aggregatedErrors.append(error) 80 | } 81 | 82 | func operationDidFinish(_ operation: Foundation.Operation, withErrors errors: [NSError]) { 83 | // For use by subclasses. 84 | } 85 | } 86 | 87 | extension GroupOperation: OperationQueueDelegate { 88 | final func operationQueue(_ operationQueue: OperationQueue, willAddOperation operation: Foundation.Operation) { 89 | assert(!finishingOperation.isFinished && !finishingOperation.isExecuting, "cannot add new operations to a group after the group has completed") 90 | 91 | /* 92 | Some operation in this group has produced a new operation to execute. 93 | We want to allow that operation to execute before the group completes, 94 | so we'll make the finishing operation dependent on this newly-produced operation. 95 | */ 96 | if operation !== finishingOperation { 97 | finishingOperation.addDependency(operation) 98 | } 99 | 100 | /* 101 | All operations should be dependent on the "startingOperation". 102 | This way, we can guarantee that the conditions for other operations 103 | will not evaluate until just before the operation is about to run. 104 | Otherwise, the conditions could be evaluated at any time, even 105 | before the internal operation queue is unsuspended. 106 | */ 107 | if operation !== startingOperation { 108 | operation.addDependency(startingOperation) 109 | } 110 | } 111 | 112 | final func operationQueue(_ operationQueue: OperationQueue, operationDidFinish operation: Foundation.Operation, withErrors errors: [NSError]) { 113 | self.serialQueue.async { 114 | self.aggregatedErrors.append(contentsOf: errors) 115 | } 116 | 117 | if operation === finishingOperation { 118 | internalQueue.isSuspended = true 119 | self.serialQueue.sync { 120 | finish(aggregatedErrors) 121 | } 122 | } 123 | else if operation !== startingOperation { 124 | operationDidFinish(operation, withErrors: errors) 125 | } 126 | } 127 | } 128 | 129 | -------------------------------------------------------------------------------- /WWDCSubGetter/Operations/Operations/Operation.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2015 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file contains the foundational subclass of NSOperation. 7 | */ 8 | 9 | import Foundation 10 | 11 | 12 | 13 | @objc private extension Operation { 14 | /** 15 | Add the "state" key to the key value observable properties of `Foundation.Operation`. 16 | */ 17 | class func keyPathsForValuesAffectingIsReady() -> Set { 18 | return ["state"] 19 | } 20 | 21 | class func keyPathsForValuesAffectingIsExecuting() -> Set { 22 | return ["state"] 23 | } 24 | 25 | class func keyPathsForValuesAffectingIsFinished() -> Set { 26 | return ["state"] 27 | } 28 | 29 | // class func keyPathsForValuesAffectingIsCancelled() -> Set { 30 | // return ["state"] 31 | // } 32 | } 33 | 34 | /** 35 | The subclass of `NSOperation` from which all other operations should be derived. 36 | This class adds both Conditions and Observers, which allow the operation to define 37 | extended readiness requirements, as well as notify many interested parties 38 | about interesting operation state changes 39 | */ 40 | class Operation: Foundation.Operation { 41 | 42 | // MARK: State Management 43 | 44 | fileprivate enum State: Int, Comparable { 45 | /// The initial state of an `Operation`. 46 | case initialized 47 | 48 | /// The `Operation` is ready to begin evaluating conditions. 49 | case pending 50 | 51 | /// The `Operation` is evaluating conditions. 52 | case evaluatingConditions 53 | 54 | /** 55 | The `Operation`'s conditions have all been satisfied, and it is ready 56 | to execute. 57 | */ 58 | case ready 59 | 60 | /// The `Operation` is executing. 61 | case executing 62 | 63 | /** 64 | Execution of the `Operation` has finished, but it has not yet notified 65 | the queue of this. 66 | */ 67 | case finishing 68 | 69 | /// The `Operation` has finished executing. 70 | case finished 71 | 72 | func canTransitionToState(_ target: State) -> Bool { 73 | switch (self, target) { 74 | case (.initialized, .pending): 75 | return true 76 | case (.pending, .evaluatingConditions): 77 | return true 78 | case (.evaluatingConditions, .ready): 79 | return true 80 | case (.ready, .executing): 81 | return true 82 | case (.ready, .finishing): 83 | return true 84 | case (.executing, .finishing): 85 | return true 86 | case (.finishing, .finished): 87 | return true 88 | default: 89 | return false 90 | } 91 | } 92 | } 93 | 94 | /** 95 | Indicates that the Operation can now begin to evaluate readiness conditions, 96 | if appropriate. 97 | */ 98 | func willEnqueue() { 99 | state = .pending 100 | } 101 | 102 | /// Private storage for the `state` property that will be KVO observed. 103 | fileprivate var _state = State.initialized 104 | 105 | /// A lock to guard reads and writes to the `_state` property 106 | fileprivate let stateLock = NSLock() 107 | 108 | fileprivate var state: State { 109 | get { 110 | return stateLock.withCriticalScope { 111 | _state 112 | } 113 | } 114 | 115 | set(newState) { 116 | /* 117 | It's important to note that the KVO notifications are NOT called from inside 118 | the lock. If they were, the app would deadlock, because in the middle of 119 | calling the `didChangeValueForKey()` method, the observers try to access 120 | properties like "isReady" or "isFinished". Since those methods also 121 | acquire the lock, then we'd be stuck waiting on our own lock. It's the 122 | classic definition of deadlock. 123 | */ 124 | 125 | // let stateKeyPath = \Operation.state 126 | // willChangeValue(for: stateKeyPath) 127 | willChangeValue(forKey: "state") 128 | 129 | stateLock.withCriticalScope { () -> Void in 130 | guard _state != .finished else { 131 | return 132 | } 133 | 134 | assert(_state.canTransitionToState(newState), "Performing invalid state transition.") 135 | _state = newState 136 | } 137 | // didChangeValue(for: stateKeyPath) 138 | didChangeValue(forKey: "state") 139 | } 140 | } 141 | 142 | // Here is where we extend our definition of "readiness". 143 | override var isReady: Bool { 144 | switch state { 145 | 146 | case .initialized: 147 | // If the operation has been cancelled, "isReady" should return true 148 | return isCancelled 149 | 150 | case .pending: 151 | // If the operation has been cancelled, "isReady" should return true 152 | guard !isCancelled else { 153 | return true 154 | } 155 | 156 | // If super isReady, conditions can be evaluated 157 | if super.isReady { 158 | evaluateConditions() 159 | } 160 | 161 | // Until conditions have been evaluated, "isReady" returns false 162 | return false 163 | 164 | case .ready: 165 | return super.isReady || isCancelled 166 | 167 | default: 168 | return false 169 | } 170 | } 171 | 172 | var userInitiated: Bool { 173 | get { 174 | return qualityOfService == .userInitiated 175 | } 176 | 177 | set { 178 | assert(state < .executing, "Cannot modify userInitiated after execution has begun.") 179 | 180 | qualityOfService = newValue ? .userInitiated : .default 181 | } 182 | } 183 | 184 | override var isExecuting: Bool { 185 | return state == .executing 186 | } 187 | 188 | override var isFinished: Bool { 189 | return state == .finished 190 | } 191 | 192 | fileprivate func evaluateConditions() { 193 | assert(state == .pending && !isCancelled, "evaluateConditions() was called out-of-order") 194 | 195 | state = .evaluatingConditions 196 | 197 | guard conditions.count > 0 else { 198 | state = .ready 199 | return 200 | } 201 | 202 | OperationConditionEvaluator.evaluate(conditions, operation: self) { failures in 203 | self._internalErrors += failures 204 | self.state = .ready 205 | } 206 | } 207 | 208 | // MARK: Observers and Conditions 209 | 210 | fileprivate(set) var conditions = [OperationCondition]() 211 | 212 | func addCondition(_ condition: OperationCondition) { 213 | assert(state < .evaluatingConditions, "Cannot modify conditions after execution has begun.") 214 | 215 | conditions.append(condition) 216 | } 217 | 218 | fileprivate(set) var observers = [OperationObserver]() 219 | 220 | func addObserver(_ observer: OperationObserver) { 221 | assert(state < .executing, "Cannot modify observers after execution has begun.") 222 | 223 | observers.append(observer) 224 | } 225 | 226 | override func addDependency(_ operation: Foundation.Operation) { 227 | assert(state < .executing, "Dependencies cannot be modified after execution has begun.") 228 | 229 | super.addDependency(operation) 230 | } 231 | 232 | // MARK: Execution and Cancellation 233 | 234 | override final func start() { 235 | // NSOperation.start() contains important logic that shouldn't be bypassed. 236 | super.start() 237 | 238 | // If the operation has been cancelled, we still need to enter the "Finished" state. 239 | if isCancelled { 240 | finish() 241 | } 242 | } 243 | 244 | override final func main() { 245 | assert(state == .ready, "This operation must be performed on an operation queue.") 246 | 247 | if _internalErrors.isEmpty && !isCancelled { 248 | state = .executing 249 | 250 | for observer in observers { 251 | observer.operationDidStart(self) 252 | } 253 | 254 | print("Operation \(self.name ?? "has no name") executed") 255 | 256 | execute() 257 | } 258 | else { 259 | finish() 260 | } 261 | } 262 | 263 | /** 264 | `execute()` is the entry point of execution for all `Operation` subclasses. 265 | If you subclass `Operation` and wish to customize its execution, you would 266 | do so by overriding the `execute()` method. 267 | 268 | At some point, your `Operation` subclass must call one of the "finish" 269 | methods defined below; this is how you indicate that your operation has 270 | finished its execution, and that operations dependent on yours can re-evaluate 271 | their readiness state. 272 | */ 273 | func execute() { 274 | print("\(type(of: self)) must override `execute()`.") 275 | 276 | finish() 277 | } 278 | 279 | fileprivate var _internalErrors = [NSError]() 280 | func cancelWithError(_ error: NSError? = nil) { 281 | if let error = error { 282 | _internalErrors.append(error) 283 | } 284 | 285 | cancel() 286 | } 287 | 288 | final func produceOperation(_ operation: Foundation.Operation) { 289 | for observer in observers { 290 | observer.operation(self, didProduceOperation: operation) 291 | } 292 | } 293 | 294 | // MARK: Finishing 295 | 296 | /** 297 | Most operations may finish with a single error, if they have one at all. 298 | This is a convenience method to simplify calling the actual `finish()` 299 | method. This is also useful if you wish to finish with an error provided 300 | by the system frameworks. As an example, see `DownloadEarthquakesOperation` 301 | for how an error from an `NSURLSession` is passed along via the 302 | `finishWithError()` method. 303 | */ 304 | final func finishWithError(_ error: NSError?) { 305 | if let error = error { 306 | finish([error]) 307 | } 308 | else { 309 | finish() 310 | } 311 | } 312 | 313 | /** 314 | A private property to ensure we only notify the observers once that the 315 | operation has finished. 316 | */ 317 | fileprivate var hasFinishedAlready = false 318 | 319 | final func finish(_ errors: [NSError] = []) { 320 | if !hasFinishedAlready { 321 | hasFinishedAlready = true 322 | state = .finishing 323 | 324 | let combinedErrors = _internalErrors + errors 325 | finished(combinedErrors) 326 | print("Operation \(self.name ?? "has no name") finished") 327 | 328 | for observer in observers { 329 | observer.operationDidFinish(self, errors: combinedErrors) 330 | } 331 | 332 | state = .finished 333 | } 334 | } 335 | 336 | /** 337 | Subclasses may override `finished(_:)` if they wish to react to the operation 338 | finishing with errors. For example, the `LoadModelOperation` implements 339 | this method to potentially inform the user about an error when trying to 340 | bring up the Core Data stack. 341 | */ 342 | func finished(_ errors: [NSError]) { 343 | // No op. 344 | } 345 | 346 | override final func waitUntilFinished() { 347 | /* 348 | Waiting on operations is almost NEVER the right thing to do. It is 349 | usually superior to use proper locking constructs, such as `dispatch_semaphore_t` 350 | or `dispatch_group_notify`, or even `NSLocking` objects. Many developers 351 | use waiting when they should instead be chaining discrete operations 352 | together using dependencies. 353 | 354 | To reinforce this idea, invoking `waitUntilFinished()` will crash your 355 | app, as incentive for you to find a more appropriate way to express 356 | the behavior you're wishing to create. 357 | */ 358 | fatalError("Waiting on operations is an anti-pattern. Remove this ONLY if you're absolutely sure there is No Other Way™.") 359 | } 360 | } 361 | 362 | // Simple operator functions to simplify the assertions used above. 363 | private func <(lhs: Operation.State, rhs: Operation.State) -> Bool { 364 | return lhs.rawValue < rhs.rawValue 365 | } 366 | 367 | private func ==(lhs: Operation.State, rhs: Operation.State) -> Bool { 368 | return lhs.rawValue == rhs.rawValue 369 | } 370 | 371 | -------------------------------------------------------------------------------- /WWDCSubGetter/Operations/Operations/URLSessionTaskOperation.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2015 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | Shows how to lift operation-like objects in to the NSOperation world. 7 | */ 8 | 9 | import Foundation 10 | 11 | private var URLSessionTaksOperationKVOContext = 0 12 | 13 | /** 14 | `URLSessionTaskOperation` is an `Operation` that lifts an `NSURLSessionTask` 15 | into an operation. 16 | 17 | Note that this operation does not participate in any of the delegate callbacks \ 18 | of an `NSURLSession`, but instead uses Key-Value-Observing to know when the 19 | task has been completed. It also does not get notified about any errors that 20 | occurred during execution of the task. 21 | 22 | An example usage of `URLSessionTaskOperation` can be seen in the `DownloadEarthquakesOperation`. 23 | */ 24 | class URLSessionTaskOperation: Operation { 25 | let task: URLSessionTask 26 | 27 | init(task: URLSessionTask) { 28 | assert(task.state == .suspended, "Tasks must be suspended.") 29 | self.task = task 30 | super.init() 31 | name = "URLSessionTaskOperation" 32 | } 33 | 34 | override func execute() { 35 | 36 | assert(task.state == .suspended, "Task was resumed by something other than \(self).") 37 | 38 | task.addObserver(self, forKeyPath: "state", options: [], context: &URLSessionTaksOperationKVOContext) 39 | 40 | task.resume() 41 | } 42 | 43 | override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { 44 | guard context == &URLSessionTaksOperationKVOContext else { return } 45 | 46 | if object as AnyObject === task && keyPath == "state" && task.state == .completed { 47 | task.removeObserver(self, forKeyPath: "state") 48 | finish() 49 | } 50 | } 51 | 52 | override func cancel() { 53 | task.cancel() 54 | super.cancel() 55 | } 56 | } 57 | 58 | -------------------------------------------------------------------------------- /WWDCSubGetter/ParseHtmlVideoPageOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ParseHtmlVideoPageOperation.swift 3 | // WWDC.srt 4 | // 5 | // Created by Seyed Samad Gholamzadeh on 6/6/18. 6 | // Copyright © 2018 Seyed Samad Gholamzadeh. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum SessionDataTypes: Hashable { 12 | 13 | 14 | case video(VideoQuality), pdf, sampleCode 15 | } 16 | 17 | class ParseHtmlVideoPageOperation: Operation { 18 | let cacheFile: URL 19 | 20 | let types: [SessionDataTypes] 21 | let sessionNumber: String 22 | 23 | 24 | init(for types: [SessionDataTypes], sessionNumber: String, cacheFile: URL) { 25 | self.cacheFile = cacheFile 26 | self.types = types 27 | self.sessionNumber = sessionNumber 28 | super.init() 29 | 30 | name = "ParseHtmlVideoPageOperation \(sessionNumber)" 31 | 32 | } 33 | 34 | override func execute() { 35 | executeOld() 36 | } 37 | 38 | func executeOld() { 39 | 40 | do { 41 | let data = try Data(contentsOf: self.cacheFile, options: Data.ReadingOptions.mappedIfSafe) 42 | 43 | let htmlText = String.init(data: data, encoding: 44 | .utf8)! 45 | // let quality: VideoQuality = self.types.contains(SessionDataTypes.video(.sd)) ? .sd : .hd 46 | 47 | // if !videoURLString.isEmpty { 48 | 49 | for type in types { 50 | 51 | switch type { 52 | case let .video(quality): 53 | let videoURLString = WWDCVideosController.getHDorSDdURLs(fromHTML: htmlText, format: quality) 54 | 55 | if quality == .hd { 56 | linksModel.hdVideosLinks.append(videoURLString) 57 | } 58 | else { 59 | linksModel.sdVideosLinks.append(videoURLString) 60 | } 61 | 62 | case .pdf: 63 | let pdfURLStrings = WWDCVideosController.getPDFResourceURL(fromHTML: htmlText) 64 | linksModel.pdfLinks.append(contentsOf: pdfURLStrings) 65 | 66 | case .sampleCode: 67 | let sampleCodesURLStrings = WWDCVideosController.getSampleCodeURL(fromHTML: htmlText) 68 | let sampleCodesURLStrings2 = WWDCVideosController.getSampleCodeURL2(fromHTML: htmlText) 69 | let sampleCodesURLStrings3 = WWDCVideosController.getSampleCodeURL3(fromHTML: htmlText) 70 | 71 | linksModel.sampleCodesLinks.append(contentsOf: sampleCodesURLStrings) 72 | linksModel.sampleCodesLinks.append(contentsOf: sampleCodesURLStrings2) 73 | linksModel.sampleCodesLinks.append(contentsOf: sampleCodesURLStrings3) 74 | 75 | 76 | } 77 | } 78 | 79 | // } 80 | // else { 81 | // try FileManager.default.removeItem(at: self.cacheFile) 82 | // } 83 | 84 | finish() 85 | 86 | } 87 | catch { 88 | print(error.localizedDescription) 89 | finish([error] as [NSError]) 90 | } 91 | 92 | } 93 | 94 | func executeNew() { 95 | 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /WWDCSubGetter/ParseSessionsListOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrseSessionsListOperation.swift 3 | // WWDC.srt 4 | // 5 | // Created by Seyed Samad Gholamzadeh on 6/6/18. 6 | // Copyright © 2018 Seyed Samad Gholamzadeh. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | class ParseSessionsListOperation: Operation { 13 | 14 | //MARK: Properties 15 | 16 | let sessionListURL: URL 17 | let wwdcYear: WWDC 18 | 19 | //MARK: Initializer 20 | 21 | init(for wwdcYear: WWDC, cacheFile: URL) { 22 | 23 | self.sessionListURL = cacheFile 24 | self.wwdcYear = wwdcYear 25 | 26 | super.init() 27 | self.name = "ParseSessionsListOperation" 28 | } 29 | 30 | override func execute() { 31 | 32 | do { 33 | 34 | var sessionsListArray: [String] = [] 35 | 36 | let data = try Data(contentsOf: self.sessionListURL, options: Data.ReadingOptions.mappedIfSafe) 37 | 38 | let htmlSessionListString = String.init(data: data, encoding: 39 | String.Encoding.utf8)! 40 | 41 | sessionsListArray = WWDCVideosController.getSessionsList(fromHTML: htmlSessionListString, wwdcYear: wwdcYear.stringValue) 42 | 43 | //get unique values 44 | sessionsListArray = Array(Set(sessionsListArray)).sorted() 45 | 46 | linksModel.titles = sessionsListArray 47 | 48 | if !linksModel.titles.isEmpty { 49 | let titlesURL = linksModel.titlesCacheURLFor(wwdcYear) 50 | let text = linksModel.titles.removingDuplicates().sorted().joined(separator: "\n") 51 | try text.write(to: titlesURL, atomically: false, encoding: String.Encoding.utf8) 52 | } 53 | 54 | } catch { 55 | 56 | print(error) 57 | } 58 | 59 | linksModel.clear() 60 | finish() 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /WWDCSubGetter/Presenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Presenter.swift 3 | // WWDC.srt 4 | // 5 | // Created by Seyed Samad Gholamzadeh on 1/22/19. 6 | // Copyright © 2019 Seyed Samad Gholamzadeh. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class Presenter { 12 | 13 | let operationQueue = OperationQueue() 14 | 15 | func convertToSubtitle(from url: String, wwdc: WWDC, type: InputType, completion: @escaping ([Subtitle]) -> Void) { 16 | let convertToSubtitleOperation = ConvertToSubtitleOperation(from: url, wwdc: wwdc, type: type, completion: completion) 17 | self.operationQueue.addOperation(convertToSubtitleOperation) 18 | } 19 | 20 | func getSubtitles(completionHandler: @escaping () -> Void) { 21 | 22 | let operation = GetSubtitlesOperation(completionHandler: completionHandler) 23 | 24 | self.operationQueue.addOperation(operation) 25 | } 26 | 27 | func getLinks(for types: [SessionDataTypes], wwdcYear: WWDC, sessionNumber: String? = nil, copyToUserDestinationURL: Bool, completionHandler: @escaping () -> Void) { 28 | 29 | let getLinksOperation = GetLinksOperation(for: types, wwdcYear: wwdcYear, sessionNumber: sessionNumber, copyToUserDestinationURL: copyToUserDestinationURL, completionHandler: completionHandler) 30 | 31 | self.operationQueue.addOperation(getLinksOperation) 32 | 33 | } 34 | 35 | func getSessionsList(for wwdcYear: WWDC, copyToUserDestinationURL: Bool, completionHandler: @escaping () -> Void) { 36 | 37 | let getSessionsListOperation = GetSessionsListOperation(for: wwdcYear, copyToUserDestinationURL: copyToUserDestinationURL, completionHandler: completionHandler) 38 | 39 | self.operationQueue.addOperation(getSessionsListOperation) 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /WWDCSubGetter/Resources/WWDC2013_links.txt: -------------------------------------------------------------------------------- 1 | http://devstreaming.apple.com/videos/wwdc/2013/710xfx3xn8197k4i9s2rvyb/710/710-HD.mov?dl=1 2 | http://devstreaming.apple.com/videos/wwdc/2013/202xdx2x47ezp1wein/202/202-HD.mov?dl=1 3 | http://devstreaming.apple.com/videos/wwdc/2013/200xdx2x35e1pxiinm/200/200-HD.mov?dl=1 4 | http://devstreaming.apple.com/videos/wwdc/2013/413xdx5x97itb5ek4yex3r7/413/413-HD.mov?dl=1 5 | http://devstreaming.apple.com/videos/wwdc/2013/612xax4xx65z1ervy5np1qb/612/612-HD.mov?dl=1 6 | http://devstreaming.apple.com/videos/wwdc/2013/221xex4xxohbllf4hblyngt/221/221-HD.mov?dl=1 7 | http://devstreaming.apple.com/videos/wwdc/2013/220xbx4xipaxfd1tggxuoib/220/220-HD.mov?dl=1 8 | http://devstreaming.apple.com/videos/wwdc/2013/711xcx4x8yuutk8sady6t9f/711/711-HD.mov?dl=1 9 | http://devstreaming.apple.com/videos/wwdc/2013/404xbx2xvp1eaaqonr8zokm/404/404-HD.mov?dl=1 10 | http://devstreaming.apple.com/videos/wwdc/2013/505xbx4xrgmhwby4oiwkrpp/505/505-HD.mov?dl=1 11 | http://devstreaming.apple.com/videos/wwdc/2013/102xex1x2e4xpykz1y/102/102-HD.mov?dl=1 12 | http://devstreaming.apple.com/videos/wwdc/2013/213xax3xr33a8oxfsxg8bha/213/213-HD.mov?dl=1 13 | http://devstreaming.apple.com/videos/wwdc/2013/225xex5xkk51ehr0z54gd9kt/225/225-HD.mov?dl=1 14 | http://devstreaming.apple.com/videos/wwdc/2013/216xcx4x7if809qdggi7vcc/216/216-HD.mov?dl=1 15 | http://devstreaming.apple.com/videos/wwdc/2013/611xfx3x2l2zzkb2oaovson/611/611-HD.mov?dl=1 16 | http://devstreaming.apple.com/videos/wwdc/2013/704xcx2xrmodatdtgs6nf5w4/704/704-HD.mov?dl=1 17 | http://devstreaming.apple.com/videos/wwdc/2013/201xex2xxf5ynwnsgl/201/201-HD.mov?dl=1 18 | http://devstreaming.apple.com/videos/wwdc/2013/412xdx4x6xpjownhu5d4as36/412/412-HD.mov?dl=1 19 | http://devstreaming.apple.com/videos/wwdc/2013/703xcx2xljtlq481grxurla4/703/703-HD.mov?dl=1 20 | http://devstreaming.apple.com/videos/wwdc/2013/211xcx3xeognrgah6anssirb/211/211-HD.mov?dl=1 21 | http://devstreaming.apple.com/videos/wwdc/2013/509xbx5xydkscnk7eyuyj1qi/509/509-HD.mov?dl=1 22 | http://devstreaming.apple.com/videos/wwdc/2013/218xdx4xj2umojkv3e8dbk6i/218/218-HD.mov?dl=1 23 | http://devstreaming.apple.com/videos/wwdc/2013/214xex3xkbq0z93doa6o31c0/214/214-HD.mov?dl=1 24 | http://devstreaming.apple.com/videos/wwdc/2013/407xdx3xw3kl5xx1h5cs73sp/407/407-HD.mov?dl=1 25 | http://devstreaming.apple.com/videos/wwdc/2013/700xcx2xuedgs1rni4qry1yumg/700/700-HD.mov?dl=1 26 | http://devstreaming.apple.com/videos/wwdc/2013/224xcx5x1y1yx8ra5jbmfyhf/224/224-HD.mov?dl=1 27 | http://devstreaming.apple.com/videos/wwdc/2013/503xbx3xoaktjug4e05xvl04/503/503-HD.mov?dl=1 28 | http://devstreaming.apple.com/videos/wwdc/2013/702xfx2xmlrics5pyrjfwu2m/702/702-HD.mov?dl=1 29 | http://devstreaming.apple.com/videos/wwdc/2013/712xax4xb4mh6prvqkesmm75/712/712-HD.mov?dl=1 30 | http://devstreaming.apple.com/videos/wwdc/2013/217xex4xi4ws3f5wrv3w2wkh/217/217-HD.mov?dl=1 31 | http://devstreaming.apple.com/videos/wwdc/2013/301xcx2xzxf8qjdcu3y2k1itm/301/301-HD.mov?dl=1 32 | http://devstreaming.apple.com/videos/wwdc/2013/410xcx4x9x0ry3zbw3rzq14kx/410/410-HD.mov?dl=1 33 | http://devstreaming.apple.com/videos/wwdc/2013/403xex2xoo3u74gemi4q9vhvs/403/403-HD.mov?dl=1 34 | http://devstreaming.apple.com/videos/wwdc/2013/206xex2xws29m12p69m7s169q/206/206-HD.mov?dl=1 35 | http://devstreaming.apple.com/videos/wwdc/2013/603xfx2xhszvvn0zgc1cbgx1j/603/603-HD.mov?dl=1 36 | http://devstreaming.apple.com/videos/wwdc/2013/601xfx2xk7wrmhwsa5gao39ln/601/601-HD.mov?dl=1 37 | http://devstreaming.apple.com/videos/wwdc/2013/310xex5xg29dwip7fgthel9z2/310/310-HD.mov?dl=1 38 | http://devstreaming.apple.com/videos/wwdc/2013/228xax5xif2s7s53df5ffjfao2/228/228-HD.mov?dl=1 39 | http://devstreaming.apple.com/videos/wwdc/2013/613xfx4xp91m26w7iu790d3nx/613/613-HD.mov?dl=1 40 | http://devstreaming.apple.com/videos/wwdc/2013/226xbx5xinmlvbdabxux9k3kt/226/226-HD.mov?dl=1 41 | http://devstreaming.apple.com/videos/wwdc/2013/614xax5x602jmihlq4c4edtrl/614/614-HD.mov?dl=1 42 | http://devstreaming.apple.com/videos/wwdc/2013/209xdx3xahizhph6dg8wk631i/209/209-HD.mov?dl=1 43 | http://devstreaming.apple.com/videos/wwdc/2013/615xax5xpcdns8jyhaiszkz2p/615/615-HD.mov?dl=1 44 | http://devstreaming.apple.com/videos/wwdc/2013/303xdx3xodi2951d3kopnrhlg/303/303-HD.mov?dl=1 45 | http://devstreaming.apple.com/videos/wwdc/2013/501xdx2xat8ocml4nv7i08noe/501/501-HD.mov?dl=1 46 | http://devstreaming.apple.com/videos/wwdc/2013/405xcx3x5ud6sopkxfqg7ikk/405/405-HD.mov?dl=1 47 | http://devstreaming.apple.com/videos/wwdc/2013/416xxx3x90yhsjxfqfbi/416/416-HD.mov?dl=1 48 | http://devstreaming.apple.com/videos/wwdc/2013/604xfx2xl2ujhx7vffbjmtqu2t/604/604-HD.mov?dl=1 49 | http://devstreaming.apple.com/videos/wwdc/2013/210xbx3xpg6jhh8okmc7zc8j89/210/210-HD.mov?dl=1 50 | http://devstreaming.apple.com/videos/wwdc/2013/609xfx3xpwcil5bz8h6ot4f871/609/609-HD.mov?dl=1 51 | http://devstreaming.apple.com/videos/wwdc/2013/502xex3x2iwfiaeglpjw0mh54u/502/502-HD.mov?dl=1 52 | http://devstreaming.apple.com/videos/wwdc/2013/100xex1xb4fuo9xiu0/100/100-HD.mov?dl=1 53 | http://devstreaming.apple.com/videos/wwdc/2013/219xax4xjor8i6b9h77lafay32/219/219-HD.mov?dl=1 54 | http://devstreaming.apple.com/videos/wwdc/2013/300xdx2xem8o4pmrhvraq9ty76/300/300-HD.mov?dl=1 55 | http://devstreaming.apple.com/videos/wwdc/2013/415xdx5x0wygxidaf0ifiey/415/415-HD.mov?dl=1 56 | http://devstreaming.apple.com/videos/wwdc/2013/701xbx2xqblo39z6tpbdrcz/701/701-HD.mov?dl=1 57 | http://devstreaming.apple.com/videos/wwdc/2013/606xdx2xbp31zp28fdov8p0b6d/606/606-HD.mov?dl=1 58 | http://devstreaming.apple.com/videos/wwdc/2013/708xbx3x7xusbzidl0j3acxest/708/708-HD.mov?dl=1 59 | http://devstreaming.apple.com/videos/wwdc/2013/408xcx3x0l4e2phvin8xrhsn23/408/408-HD.mov?dl=1 60 | http://devstreaming.apple.com/videos/wwdc/2013/215xax3xz5pbbxeaxxe7z1mk3q/215/215-HD.mov?dl=1 61 | http://devstreaming.apple.com/videos/wwdc/2013/417xxx0oh4r99eed2hb1k3ce/417/417-HD.mov?dl=1 62 | http://devstreaming.apple.com/videos/wwdc/2013/109xxxjfr9zgakbgrzxk23n2/109/109-HD.mov?dl=1 63 | http://devstreaming.apple.com/videos/wwdc/2013/101xex1x82z7rpszsw/101/101-HD.mov?dl=1 64 | http://devstreaming.apple.com/videos/wwdc/2013/607xfx3xox1jscc7z24fl2jf4mf/607/607-HD.mov?dl=1 65 | http://devstreaming.apple.com/videos/wwdc/2013/608xcx3xafjdvmnjpo5dzvou97r/608/608-HD.mov?dl=1 66 | http://devstreaming.apple.com/videos/wwdc/2013/709xax3xiafkagts5jfa5705dx6/709/709-HD.mov?dl=1 67 | http://devstreaming.apple.com/videos/wwdc/2013/714xbx5xcen10zkjl5f9sd8ys63/714/714-HD.mov?dl=1 68 | http://devstreaming.apple.com/videos/wwdc/2013/309xdx4x891fj1ing58e5cayt1z/309/309-HD.mov?dl=1 69 | http://devstreaming.apple.com/videos/wwdc/2013/227xax5xif2s7s531dsmfs1afo2/227/227-HD.mov?dl=1 70 | http://devstreaming.apple.com/videos/wwdc/2013/406xex3x90a7ka0kyhsfjxfqfbi/406/406-HD.mov?dl=1 71 | http://devstreaming.apple.com/videos/wwdc/2013/409xdx4x4adcwca5ok0rtsup0sg/409/409-HD.mov?dl=1 72 | http://devstreaming.apple.com/videos/wwdc/2013/713xcx4xtaue02i1tvk0kpvarvo/713/713-HD.mov?dl=1 73 | http://devstreaming.apple.com/videos/wwdc/2013/506xbx4x8brixcxa41wrzgph0gw/506/506-HD.mov?dl=1 74 | http://devstreaming.apple.com/videos/wwdc/2013/414xdx5xbjc8ls04ewrox0a160i/414/414-HD.mov?dl=1 75 | http://devstreaming.apple.com/videos/wwdc/2013/223xex5xsgdfh1ergtjrqwoghbj/223/223-HD.mov?dl=1 76 | http://devstreaming.apple.com/videos/wwdc/2013/308xex4x6ybggtlw4ztv0sg5btp/308/308-HD.mov?dl=1 77 | http://devstreaming.apple.com/videos/wwdc/2013/305xbx4xqongltzvuja8xrmsrq1/305/305-HD.mov?dl=1 78 | http://devstreaming.apple.com/videos/wwdc/2013/610xcx3xv9xjy916g3wzes0ze63/610/610-HD.mov?dl=1 79 | http://devstreaming.apple.com/videos/wwdc/2013/203xex2xro3o27pyntvhsqsohil/203/203-HD.mov?dl=1 80 | http://devstreaming.apple.com/videos/wwdc/2013/205xbx2xfbtmyu1l18h36mfqxmy/205/205-HD.mov?dl=1 81 | http://devstreaming.apple.com/videos/wwdc/2013/602xcx2xk6ipx0cusjryu1sx5eu/602/602-HD.mov?dl=1 82 | http://devstreaming.apple.com/videos/wwdc/2013/207xdx3xbarjw2d2va5olp57qh8/207/207-HD.mov?dl=1 83 | http://devstreaming.apple.com/videos/wwdc/2013/307xex4xl1ey243ksyxqfip0xowr/307/307-HD.mov?dl=1 84 | http://devstreaming.apple.com/videos/wwdc/2013/705xbx3xcjsmrdbtwl5grta6gq6r/705/705-HD.mov?dl=1 85 | http://devstreaming.apple.com/videos/wwdc/2013/504xbx3x55lc470bv6s8dk2lcg28/504/504-HD.mov?dl=1 86 | http://devstreaming.apple.com/videos/wwdc/2013/605xfx2xbot31cme1uns8w647vsw/605/605-HD.mov?dl=1 87 | http://devstreaming.apple.com/videos/wwdc/2013/208xex3xyxmpz8s37fk59avul0c5/208/208-HD.mov?dl=1 88 | http://devstreaming.apple.com/videos/wwdc/2013/306xdx4xq2n1jhue4dfou0nemckj/306/306-HD.mov?dl=1 89 | http://devstreaming.apple.com/videos/wwdc/2013/707xfx3xysb4yyrvtxbbu6t9fono/707/707-HD.mov?dl=1 90 | http://devstreaming.apple.com/videos/wwdc/2013/304xex4x7qun15qmlzgf1s39fn08/304/304-HD.mov?dl=1 91 | http://devstreaming.apple.com/videos/wwdc/2013/507xax4xgxp9uuxl9i7y0emdv5ib/507/507-HD.mov?dl=1 92 | http://devstreaming.apple.com/videos/wwdc/2013/302xdx3xf65k8c0kkajsjy2mh9jj/302/302-HD.mov?dl=1 93 | http://devstreaming.apple.com/videos/wwdc/2013/600xbx2x3eaj3ryz3w5zw5gozz39/600/600-HD.mov?dl=1 94 | http://devstreaming.apple.com/videos/wwdc/2013/500xbx2xh3dv43mpm46sd0qzj8d0/500/500-HD.mov?dl=1 95 | http://devstreaming.apple.com/videos/wwdc/2013/222xbx4xitmr47hmc2ulz1tli4hv/222/222-HD.mov?dl=1 96 | http://devstreaming.apple.com/videos/wwdc/2013/402xdx2x3ccrfzk85j9dysimvsui/402/402-HD.mov?dl=1 97 | http://devstreaming.apple.com/videos/wwdc/2013/400xex2xbskwa5bkxr17zihju9uf/400/400-HD.mov?dl=1 98 | http://devstreaming.apple.com/videos/wwdc/2013/204xex2xvpdncz9kdb17lmfooh/204/204-HD.mov?dl=1 99 | http://devstreaming.apple.com/videos/wwdc/2013/508xax4xqkee4rd9rsmbb86dw5cu/508/508-HD.mov?dl=1 100 | http://devstreaming.apple.com/videos/wwdc/2013/401xbx2xq50gv7mqafyfdns2yhxl/401/401-HD.mov?dl=1 101 | -------------------------------------------------------------------------------- /WWDCSubGetter/Resources/WWDC2014_links.txt: -------------------------------------------------------------------------------- 1 | http://devstreaming.apple.com/videos/wwdc/2014/403xxksrj0qs8c0/403/403_hd_intermediate_swift.mov?dl=1 2 | http://devstreaming.apple.com/videos/wwdc/2014/419xxli6f60a6bs/419/419_hd_advanced_graphics_and_animation_performance.mov?dl=1 3 | http://devstreaming.apple.com/videos/wwdc/2014/101xx36lr6smzjo/101/101_hd.mov?dl=1 4 | http://devstreaming.apple.com/videos/wwdc/2014/236xxwk3fv82sx2/236/236_hd_building_interruptible_and_responsive_interactions.mov?dl=1 5 | http://devstreaming.apple.com/videos/wwdc/2014/306xxjtg7uz13v0/306/306_hd_javascript_for_automation.mov?dl=1 6 | http://devstreaming.apple.com/videos/wwdc/2014/404xxdxsstkaqjb/404/404_hd_advanced_swift.mov?dl=1 7 | http://devstreaming.apple.com/videos/wwdc/2014/701xx8n8ca3aq4j/701/701_hd_designing_accessories_for_ios_and_os_x.mov?dl=1 8 | http://devstreaming.apple.com/videos/wwdc/2014/224xxxlsvigdoc0/224/224_hd_core_os_ios_application_architectural_patterns.mov?dl=1 9 | http://devstreaming.apple.com/videos/wwdc/2014/717xxux5eg6f9v4/717/717_hd_kids_and_apps.mov?dl=1 10 | http://devstreaming.apple.com/videos/wwdc/2014/716xx8q4shlqcp8/716/716_hd_power_performance_and_diagnostics_whats_new_in_gcd_and_xpc_for.mov?dl=1 11 | http://devstreaming.apple.com/videos/wwdc/2014/712xx1pl2u942g2/712/712_hd_writing_energy_efficient_code_part_2.mov?dl=1 12 | http://devstreaming.apple.com/videos/wwdc/2014/710xxwwk9jiqtu2/710/710_hd_writing_energy_efficient_code_part_1.mov?dl=1 13 | http://devstreaming.apple.com/videos/wwdc/2014/713xx1il4h4ur9c/713/713_hd_whats_new_in_ios_notifications.mov?dl=1 14 | http://devstreaming.apple.com/videos/wwdc/2014/705xx0r0x0fsaf5/705/705_hd_distributing_enterprise_apps.mov?dl=1 15 | http://devstreaming.apple.com/videos/wwdc/2014/704xx7dmqd5m9l4/704/704_hd_building_apps_for_enterprise_and_education.mov?dl=1 16 | http://devstreaming.apple.com/videos/wwdc/2014/702xxvsjwkmhw2e/702/702_hd_managing_apple_devices.mov?dl=1 17 | http://devstreaming.apple.com/videos/wwdc/2014/408xxcm26svis12/408/408_hd_swift_playgrounds.mov?dl=1 18 | http://devstreaming.apple.com/videos/wwdc/2014/410xx1s19e83i5z/410/410_hd_advanced_swift_debugging_in_lldb.mov?dl=1 19 | http://devstreaming.apple.com/videos/wwdc/2014/407xxptt888z5jv/407/407_hd_swift_interoperability_in_depth.mov?dl=1 20 | http://devstreaming.apple.com/videos/wwdc/2014/216xxcnxc6wnkf3/216/216_hd_building_adaptive_apps_with_uikit.mov?dl=1 21 | http://devstreaming.apple.com/videos/wwdc/2014/226xxf6phq7ufzl/226/226_hd_whats_new_in_table_and_collection_views.mov?dl=1 22 | http://devstreaming.apple.com/videos/wwdc/2014/209xxbyg01mfqt8/209/209_hd_adapting_your_app_to_the_new_ui_of_os_x_yosemite.mov?dl=1 23 | http://devstreaming.apple.com/videos/wwdc/2014/220xx01yweszmjv/220/220_hd_adopting_advanced_features_of_the_new_ui_of_os_x_yosemite.mov?dl=1 24 | http://devstreaming.apple.com/videos/wwdc/2014/212xxi1kzzkdr54/212/212_hd_storyboards_and_controllers_on_os_x.mov?dl=1 25 | http://devstreaming.apple.com/videos/wwdc/2014/204xxhe1lli87dm/204/204_hd_whats_new_in_cocoa.mov?dl=1 26 | http://devstreaming.apple.com/videos/wwdc/2014/235xxsugqo8pxak/235/235_hd_advanced_scrollviews_and_touch_handling_techniques.mov?dl=1 27 | http://devstreaming.apple.com/videos/wwdc/2014/214xxq2mdbtmp23/214/214_hd_view_controller_advancements_in_ios_8.mov?dl=1 28 | http://devstreaming.apple.com/videos/wwdc/2014/237xxcyp7vhx2xt/237/237_hd_a_strategy_for_great_work.mov?dl=1 29 | http://devstreaming.apple.com/videos/wwdc/2014/228xxnfgueiskhi/228/228_hd_a_look_inside_presentation_controllers.mov?dl=1 30 | http://devstreaming.apple.com/videos/wwdc/2014/234xxi5cismq5hn/234/234_hd_building_a_document_based_app.mov?dl=1 31 | http://devstreaming.apple.com/videos/wwdc/2014/416xxuit620s53g/416/416_hd_building_modern_frameworks.mov?dl=1 32 | http://devstreaming.apple.com/videos/wwdc/2014/415xx83xkyr55fj/415/415_hd_continuous_integration_with_xcode_6.mov?dl=1 33 | http://devstreaming.apple.com/videos/wwdc/2014/414xx4l5du0f408/414/414_hd_testing_in_xcode_6.mov?dl=1 34 | http://devstreaming.apple.com/videos/wwdc/2014/413xxr7gdc60u2p/413/413_hd_debugging_in_xcode_6.mov?dl=1 35 | http://devstreaming.apple.com/videos/wwdc/2014/412xx80au1lrfcn/412/412_hd_localizing_with_xcode_6.mov?dl=1 36 | http://devstreaming.apple.com/videos/wwdc/2014/411xx0xo98zzoor/411/411_hd_whats_new_in_interface_builder.mov?dl=1 37 | http://devstreaming.apple.com/videos/wwdc/2014/401xxfkzfrjyb93/401/401_hd_whats_new_in_xcode_6.mov?dl=1 38 | http://devstreaming.apple.com/videos/wwdc/2014/301xxu0xo3hhg9h/301/301_hd_affiliate_tools_for_app_developers.mov?dl=1 39 | http://devstreaming.apple.com/videos/wwdc/2014/417xx2zsyyp8zcs/417/417_hd_whats_new_in_llvm.mov?dl=1 40 | http://devstreaming.apple.com/videos/wwdc/2014/406xxssvkspk997/406/406_hd_integrating_swift_with_objective_c.mov?dl=1 41 | http://devstreaming.apple.com/videos/wwdc/2014/402xxgg8o88ulsr/402/402_hd_introduction_to_swift.mov?dl=1 42 | http://devstreaming.apple.com/videos/wwdc/2014/409xxfw34pado34/409/409_hd_introduction_to_lldb_and_the_swift_repl.mov?dl=1 43 | http://devstreaming.apple.com/videos/wwdc/2014/418xxtihju1a7v4/418/418_hd_improving_your_app_with_instruments.mov?dl=1 44 | http://devstreaming.apple.com/videos/wwdc/2014/225xxgzhqylosff/225/225_hd_whats_new_in_core_data.mov?dl=1 45 | http://devstreaming.apple.com/videos/wwdc/2014/231xx9bil1zgee7/231/231_hd_advanced_cloudkit.mov?dl=1 46 | http://devstreaming.apple.com/videos/wwdc/2014/208xx42tf0hw3vv/208/208_hd_introducing_cloudkit.mov?dl=1 47 | http://devstreaming.apple.com/videos/wwdc/2014/233xxwktnowwj0u/233/233_hd_sharing_code_between_ios_and_os_x.mov?dl=1 48 | http://devstreaming.apple.com/videos/wwdc/2014/509xxwli42i4gs6/509/509_hd_creating_3d_interactive_content_with_webgl.mov?dl=1 49 | http://devstreaming.apple.com/videos/wwdc/2014/506xxeo80e5kykp/506/506_hd_ensuring_continuity_between_your_app_and_web_site.mov?dl=1 50 | http://devstreaming.apple.com/videos/wwdc/2014/512xxj53iuolu78/512/512_hd_web_inspector_and_modern_javascript.mov?dl=1 51 | http://devstreaming.apple.com/videos/wwdc/2014/517xxlpmjgeqazd/517/517_hd_designing_responsive_web_experiences.mov?dl=1 52 | http://devstreaming.apple.com/videos/wwdc/2014/206xxdiurnffagr/206/206_hd_introducing_the_modern_webkit_api.mov?dl=1 53 | http://devstreaming.apple.com/videos/wwdc/2014/504xx5n1n7eie65/504/504_hd_advanced_media_for_the_web.mov?dl=1 54 | http://devstreaming.apple.com/videos/wwdc/2014/516xxanja9ziaar/516/516_hd_improving_the_accessibility_and_usability_of_complex_web_applications.mov?dl=1 55 | http://devstreaming.apple.com/videos/wwdc/2014/510xx6yeo9go0lo/510/510_hd_whats_new_in_iad_workbench.mov?dl=1 56 | http://devstreaming.apple.com/videos/wwdc/2014/222xxz9991l36ro/222/222_hd_optimize_your_earning_power_with_iad.mov?dl=1 57 | http://devstreaming.apple.com/videos/wwdc/2014/718xxctf8ley20j/718/718_hd_adopting_airprint.mov?dl=1 58 | http://devstreaming.apple.com/videos/wwdc/2014/703xx9ich9e8mkn/703/703_hd_whats_new_in_the_accelerate_framework.mov?dl=1 59 | http://devstreaming.apple.com/videos/wwdc/2014/714xx1h4szxdnyz/714/714_hd_fix_bugs_faster_using_activity_tracing.mov?dl=1 60 | http://devstreaming.apple.com/videos/wwdc/2014/707xx1o5tdjnvg9/707/707_hd_whats_new_in_foundation_networking.mov?dl=1 61 | http://devstreaming.apple.com/videos/wwdc/2014/709xx1q8hdvo14x/709/709_hd_cross_platform_nearby_networking.mov?dl=1 62 | http://devstreaming.apple.com/videos/wwdc/2014/715xx4loqo5can9/715/715_hd_user_privacy_in_ios_and_os_x.mov?dl=1 63 | http://devstreaming.apple.com/videos/wwdc/2014/711xx6j5wzufu78/711/711_hd_keychain_and_authentication_with_touch_id.mov?dl=1 64 | http://devstreaming.apple.com/videos/wwdc/2014/708xxvfd08bdof2/708/708_hd_taking_core_location_indoors.mov?dl=1 65 | http://devstreaming.apple.com/videos/wwdc/2014/706xxjytntg51wd/706/706_hd_whats_new_in_core_location.mov?dl=1 66 | http://devstreaming.apple.com/videos/wwdc/2014/211xxmyz80g30i9/211/211_hd_designing_intuitive_user_experiences.mov?dl=1 67 | http://devstreaming.apple.com/videos/wwdc/2014/210xxksa9s9ewsa/210/210_hd_accessibility_on_ios.mov?dl=1 68 | http://devstreaming.apple.com/videos/wwdc/2014/207xx270npvffao/207/207_hd_accessibility_on_os_x.mov?dl=1 69 | http://devstreaming.apple.com/videos/wwdc/2014/202xx3ane09vxdz/202/202_hd_whats_new_in_cocoa_touch.mov?dl=1 70 | http://devstreaming.apple.com/videos/wwdc/2014/229xx77tq0pmkwo/229/229_hd_advanced_ios_architecture_and_patterns.mov?dl=1 71 | http://devstreaming.apple.com/videos/wwdc/2014/219xxebl6quoz05/219/219_hd_adopting_handoff_on_ios_and_os_x.mov?dl=1 72 | http://devstreaming.apple.com/videos/wwdc/2014/232xxz8gxpbstio/232/232_hd_advanced_user_interfaces_with_collection_views.mov?dl=1 73 | http://devstreaming.apple.com/videos/wwdc/2014/227xx1g0sa1mhjf/227/227_hd_creating_modern_cocoa_apps.mov?dl=1 74 | http://devstreaming.apple.com/videos/wwdc/2014/217xxsvxdga3rh5/217/217_hd_creating_extensions_for_ios_and_os_x_part_2.mov?dl=1 75 | http://devstreaming.apple.com/videos/wwdc/2014/205xxqzduadzo14/205/205_hd_creating_extensions_for_ios_and_os_x,_part_1.mov?dl=1 76 | http://devstreaming.apple.com/videos/wwdc/2014/221xxobzcm2j26x/221/221_hd_creating_custom_ios_user_interfaces.mov?dl=1 77 | http://devstreaming.apple.com/videos/wwdc/2014/201xx2xfazhzce8/201/201_hd_advanced_topics_in_internationalization.mov?dl=1 78 | http://devstreaming.apple.com/videos/wwdc/2014/223xxp1uag2jn3n/223/223_hd_prototyping_fake_it_till_you_make_it.mov?dl=1 79 | http://devstreaming.apple.com/videos/wwdc/2014/230xxe44dq1m2da/230/230_hd_making_a_great_first_impression_with_strong_onboarding_design.mov?dl=1 80 | http://devstreaming.apple.com/videos/wwdc/2014/218xx267rleu4n8/218/218_hd_designing_a_great_in-app_purchase_experience.mov?dl=1 81 | http://devstreaming.apple.com/videos/wwdc/2014/304xxc65wjxydj8/304/304_hd_creating_great_app_previews.mov?dl=1 82 | http://devstreaming.apple.com/videos/wwdc/2014/305xxjjl70ix0y1/305/305_hd_preventing_unauthorized_purchases_with_receipts.mov?dl=1 83 | http://devstreaming.apple.com/videos/wwdc/2014/303xxqw0yb14u0r/303/303_hd_optimizing_in-app_purchases.mov?dl=1 84 | http://devstreaming.apple.com/videos/wwdc/2014/302xxo8xxixuera/302/302_hd_the_new_itunes_connect.mov?dl=1 85 | http://devstreaming.apple.com/videos/wwdc/2014/511xxegx4yyhbt9/511/511_hd_introducing_the_photos_frameworks.mov?dl=1 86 | http://devstreaming.apple.com/videos/wwdc/2014/515xxv01d9tcg3o/515/515_hd_developing_core_image_filters_for_ios.mov?dl=1 87 | http://devstreaming.apple.com/videos/wwdc/2014/514xxio5buvlu16/514/514_hd_advances_in_core_image.mov?dl=1 88 | http://devstreaming.apple.com/videos/wwdc/2014/502xxvo7vov799k/502/502_hd_avaudioengine_in_practice.mov?dl=1 89 | http://devstreaming.apple.com/videos/wwdc/2014/501xxfo4d68h054/501/501_hd_whats_new_in_core_audio.mov?dl=1 90 | http://devstreaming.apple.com/videos/wwdc/2014/508xxfvaehrll14/508/508_hd_camera_capture_manual_controls.mov?dl=1 91 | http://devstreaming.apple.com/videos/wwdc/2014/513xxhfudagscto/513/513_hd_direct_access_to_media_encoding_and_decoding.mov?dl=1 92 | http://devstreaming.apple.com/videos/wwdc/2014/505xx5j7n7h3a1q/505/505_hd_harnessing_metadata_in_audiovisual_media.mov?dl=1 93 | http://devstreaming.apple.com/videos/wwdc/2014/503xx50xm4n63qe/503/503_hd_mastering_modern_media_playback.mov?dl=1 94 | http://devstreaming.apple.com/videos/wwdc/2014/602xx0qpmdkc2ki/602/602_hd_ingredients_of_great_games.mov?dl=1 95 | http://devstreaming.apple.com/videos/wwdc/2014/612xxnsoq5fis79/612/612_hd_motion_tracking_with_the_core_motion_framework.mov?dl=1 96 | http://devstreaming.apple.com/videos/wwdc/2014/601xxu3eg5ttkcj/601/601_hd_harnessing_the_power_of_the_mac_pro_with_opengl_and_opencl.mov?dl=1 97 | http://devstreaming.apple.com/videos/wwdc/2014/605xxygcz4pd0h6/605/605_hd_working_with_metal_advanced.mov?dl=1 98 | http://devstreaming.apple.com/videos/wwdc/2014/604xxg7crkljcr8/604/604_hd_working_with_metal_fundamentals.mov?dl=1 99 | http://devstreaming.apple.com/videos/wwdc/2014/603xx33n8igr5n1/603/603_hd_working_with_metal_overview.mov?dl=1 100 | http://devstreaming.apple.com/videos/wwdc/2014/610xxc04fgmv80x/610/610_hd_building_a_game_with_scenekit.mov?dl=1 101 | http://devstreaming.apple.com/videos/wwdc/2014/609xxkxq1v95fju/609/609_hd_whats_new_in_scenekit.mov?dl=1 102 | http://devstreaming.apple.com/videos/wwdc/2014/608xx0tzmkcqkrn/608/608_hd_best_practices_for_building_spritekit_games.mov?dl=1 103 | http://devstreaming.apple.com/videos/wwdc/2014/606xxql3qoibema/606/606_hd_whats_new_in_sprite_kit.mov?dl=1 104 | http://devstreaming.apple.com/videos/wwdc/2014/611xxblieag1z2a/611/611_hd_designing_for_game_controllers.mov?dl=1 105 | http://devstreaming.apple.com/videos/wwdc/2014/203xxh9oqtm0piw/203/203_hd_introducing_healthkit.mov?dl=1 106 | http://devstreaming.apple.com/videos/wwdc/2014/213xxbmca4in9u5/213/213_hd_introducing_homekit.mov?dl=1 107 | http://devstreaming.apple.com/videos/wwdc/2014/103xx8s53gk94hl/103/103_hd_apple_design_awards.mov?dl=1 108 | http://devstreaming.apple.com/videos/wwdc/2014/102xxw2o82y78a4/102/102_hd_platforms_state_of_the_union.mov?dl=1 -------------------------------------------------------------------------------- /WWDCSubGetter/Resources/WWDC2015_links.txt: -------------------------------------------------------------------------------- 1 | http://devstreaming.apple.com/videos/wwdc/2015/217wu453thu1r1/217/217_hd_adopting_new_trackpad_features.mp4?dl=1 2 | http://devstreaming.apple.com/videos/wwdc/2015/2267p2ni281ba/226/226_hd_advanced_nsoperations.mp4?dl=1 3 | http://devstreaming.apple.com/videos/wwdc/2015/233l9q8hj9mw/233/233_hd_advanced_touch_input_on_ios.mp4?dl=1 4 | http://devstreaming.apple.com/videos/wwdc/2015/224o6pqmtb4ik/224/224_hd_app_extension_best_practices.mp4?dl=1 5 | http://devstreaming.apple.com/videos/wwdc/2015/2048w4vdjhe1i1m/204/204_hd_apple_watch_accessibility.mp4?dl=1 6 | http://devstreaming.apple.com/videos/wwdc/2015/232f1zopzycv/232/232_hd_best_practices_for_progress_reporting.mp4?dl=1 7 | http://devstreaming.apple.com/videos/wwdc/2015/213w6grumlfm0q/213/213_hd_building_apps_with_researchkit.mp4?dl=1 8 | http://devstreaming.apple.com/videos/wwdc/2015/234reaz1byqc/234/234_hd_building_document_based_apps.mp4?dl=1 9 | http://devstreaming.apple.com/videos/wwdc/2015/2313dt427pmq/231/231_hd_cocoa_touch_best_practices.mp4?dl=1 10 | http://devstreaming.apple.com/videos/wwdc/2015/209c9277tttlt9/209/209_hd_creating_complications_with_clockkit.mp4?dl=1 11 | http://devstreaming.apple.com/videos/wwdc/2015/2058z3fx76huw93/205/205_hd_getting_started_with_multitasking_on_ipad_in_ios_9.mp4?dl=1 12 | http://devstreaming.apple.com/videos/wwdc/2015/22160ycymy0qg/221/221_hd_improving_the_full_screen_window_experience.mp4?dl=1 13 | http://devstreaming.apple.com/videos/wwdc/2015/214dh5q5d0kswh/214/214_hd_introducing_on_demand_resources.mp4?dl=1 14 | http://devstreaming.apple.com/videos/wwdc/2015/5048tyhotl6/504/504_hd_introducing_safari_view_controller.mp4?dl=1 15 | http://devstreaming.apple.com/videos/wwdc/2015/223rmo6dv9hxh/223/223_hd_introducing_the_contacts_framework_for_ios_and_os_x.mp4?dl=1 16 | http://devstreaming.apple.com/videos/wwdc/2015/216isrjt4ku9w4/216/216_hd_layout_and_animation_techniques_for_watchkit.mp4?dl=1 17 | http://devstreaming.apple.com/videos/wwdc/2015/211dseo3cn0bnw/211/211_hd_multitasking_essentials_for_mediabased_apps_on_ipad_in_ios_9.mp4?dl=1 18 | http://devstreaming.apple.com/videos/wwdc/2015/2187le7kpyhdff/218/218_hd_mysteries_of_auto_layout_part_1.mp4?dl=1 19 | http://devstreaming.apple.com/videos/wwdc/2015/219u3bqgvsz2g/219/219_hd_mysteries_of_auto_layout_part_2.mp4?dl=1 20 | http://devstreaming.apple.com/videos/wwdc/2015/222ngkqh58b52/222/222_hd_new_uikit_support_for_international_user_interfaces.mp4?dl=1 21 | http://devstreaming.apple.com/videos/wwdc/2015/212mm5ra3oau66/212/212_hd_optimizing_your_app_for_multitasking_on_ipad_in_ios_9.mp4?dl=1 22 | http://devstreaming.apple.com/videos/wwdc/2015/230wt8hs0wt8/230/230_hd_performance_on_ios_and_watchos.mp4?dl=1 23 | http://devstreaming.apple.com/videos/wwdc/2015/5091mxk00t/509/509_hd_seamless_linking_to_your_app.mp4?dl=1 24 | http://devstreaming.apple.com/videos/wwdc/2015/207id8oiaxrt6lh/207/207_hd_watchkit_indepth_part_1.mp4?dl=1 25 | http://devstreaming.apple.com/videos/wwdc/2015/208wc2mdvock1md/208/208_hd_watchkit_indepth_part_2.mp4?dl=1 26 | http://devstreaming.apple.com/videos/wwdc/2015/228eahxjbaops/228/228_hd_watchkit_tips_and_tricks.mp4?dl=1 27 | http://devstreaming.apple.com/videos/wwdc/2015/2023wpov1sxpnf9/202/202_hd_whats_new_in_cocoa.mp4?dl=1 28 | http://devstreaming.apple.com/videos/wwdc/2015/220lgx5lvphj2/220/220_hd_whats_new_in_core_data.mp4?dl=1 29 | http://devstreaming.apple.com/videos/wwdc/2015/203bxvbtrom9t1t/203/203_hd_whats_new_in_healthkit.mp4?dl=1 30 | http://devstreaming.apple.com/videos/wwdc/2015/210oyq5peqlavb/210/210_hd_whats_new_in_homekit.mp4?dl=1 31 | http://devstreaming.apple.com/videos/wwdc/2015/227s0ti458qgg/227/227_hd_whats_new_in_internationalization.mp4?dl=1 32 | http://devstreaming.apple.com/videos/wwdc/2015/206v5ce46maax7s/206/206_hd_whats_new_in_mapkit.mp4?dl=1 33 | http://devstreaming.apple.com/videos/wwdc/2015/225629tzulwe0/225/225_hd_whats_new_in_nscollectionview.mp4?dl=1 34 | http://devstreaming.apple.com/videos/wwdc/2015/215972d0hjjcfx/215/215_hd_whats_new_in_storyboards.mp4?dl=1 35 | http://devstreaming.apple.com/videos/wwdc/2015/229fksrj39nd/229/229_hd_whats_new_in_uikit_dynamics_and_visual_effects.mp4?dl=1 36 | http://devstreaming.apple.com/videos/wwdc/2015/201pps6n6g0nsnz/201/201_hd_ios_accessibility.mp4?dl=1 37 | http://devstreaming.apple.com/videos/wwdc/2015/805yjy11epjkgmnn11/805/805_hd_apple_watch_design_tips_and_tricks.mp4?dl=1 38 | http://devstreaming.apple.com/videos/wwdc/2015/802mpzd3nzovlygpbg/802/802_hd_designing_for_apple_watch.mp4?dl=1 39 | http://devstreaming.apple.com/videos/wwdc/2015/801auxyvb1pgtkufjk/801/801_hd_designing_for_future_hardware.mp4?dl=1 40 | http://devstreaming.apple.com/videos/wwdc/2015/803q4kw6eqia7ssp17/803/803_hd_designing_with_animation.mp4?dl=1 41 | http://devstreaming.apple.com/videos/wwdc/2015/804eub264zh3x4j9yb/804/804_hd_introducing_the_new_system_fonts.mp4?dl=1 42 | http://devstreaming.apple.com/videos/wwdc/2015/413eflf3lrh1tyo/413/413_hd_advanced_debugging_and_the_address_sanitizer.mp4?dl=1 43 | http://devstreaming.apple.com/videos/wwdc/2015/404l17yio30l549x/404/404_hd_app_thinning_in_xcode.mp4?dl=1 44 | http://devstreaming.apple.com/videos/wwdc/2015/405t69ymgwkmfzc9/405/405_hd_authoring_rich_playgrounds.mp4?dl=1 45 | http://devstreaming.apple.com/videos/wwdc/2015/414sklk5h2k3ki3/414/414_hd_building_better_apps_with_value_types_in_swift.mp4?dl=1 46 | http://devstreaming.apple.com/videos/wwdc/2015/41097fby32x3opk/410/410_hd_continuous_integration_and_code_coverage_in_xcode.mp4?dl=1 47 | http://devstreaming.apple.com/videos/wwdc/2015/4072909wwb9o9j3/407/407_hd_implementing_ui_designs_in_interface_builder.mp4?dl=1 48 | http://devstreaming.apple.com/videos/wwdc/2015/403l7ohdidhmnkgx/403/403_hd_improving_your_existing_apps_with_swift.mp4?dl=1 49 | http://devstreaming.apple.com/videos/wwdc/2015/4097c25o0qhs6g5/409/409_hd_optimizing_swift_performance.mp4?dl=1 50 | http://devstreaming.apple.com/videos/wwdc/2015/412rhea5amj6iaf/412/412_hd_profiling_in_depth.mp4?dl=1 51 | http://devstreaming.apple.com/videos/wwdc/2015/408509vyudbqvts/408/408_hd_protocoloriented_programming_in_swift.mp4?dl=1 52 | http://devstreaming.apple.com/videos/wwdc/2015/401gee20yy5v2men/401/401_hd_swift_and_objectivec_interoperability.mp4?dl=1 53 | http://devstreaming.apple.com/videos/wwdc/2015/4119flfsnsgmlfy/411/411_hd_swift_in_practice.mp4?dl=1 54 | http://devstreaming.apple.com/videos/wwdc/2015/406o0doszwo8r15m/406/406_hd_ui_testing_in_xcode.mp4?dl=1 55 | http://devstreaming.apple.com/videos/wwdc/2015/402c0mhxbs8tt52t/402/402_hd_whats_new_in_lldb.mp4?dl=1 56 | http://devstreaming.apple.com/videos/wwdc/2015/303qzm09e7/303/303_hd_getting_the_most_out_of_app_analytics.mp4?dl=1 57 | http://devstreaming.apple.com/videos/wwdc/2015/306vjwcqnm/306/306_hd_supporting_the_enterprise_with_os_x_automation.mp4?dl=1 58 | http://devstreaming.apple.com/videos/wwdc/2015/301tcfp66f/301/301_hd_whats_new_in_managing_apple_devices.mp4?dl=1 59 | http://devstreaming.apple.com/videos/wwdc/2015/302sxabxp0/302/302_hd_whats_new_in_itunes_connect.mp4?dl=1 60 | http://devstreaming.apple.com/videos/wwdc/2015/304ywrr62d/304/304_hd_itunes_connect_development_to_distribution.mp4?dl=1 61 | http://devstreaming.apple.com/videos/wwdc/2015/103ot7lzkdri2fvn1iyh/103/103_hd_apple_design_awards.mp4?dl=1 62 | http://devstreaming.apple.com/videos/wwdc/2015/1086gvs7f4vryixs49s6/108/108_hd_building_watch_apps.mp4?dl=1 63 | http://devstreaming.apple.com/videos/wwdc/2015/105ncyldc6ofunvsgtan/105/105_hd_introducing_watchkit_for_watchos_2.mp4?dl=1 64 | http://devstreaming.apple.com/videos/wwdc/2015/1014o78qhj07pbfxt9g7/101/101_hd_keynote.mp4?dl=1 65 | http://devstreaming.apple.com/videos/wwdc/2015/1026npwuy2crj2xyuq11/102/102_hd_platforms_state_of_the_union.mp4?dl=1 66 | http://devstreaming.apple.com/videos/wwdc/2015/1075hpxmc54818sn59su/107/107_hd_whats_new_in_cocoa_touch.mp4?dl=1 67 | http://devstreaming.apple.com/videos/wwdc/2015/106z3yjwpfymnauri96m/106/106_hd_whats_new_in_swift.mp4?dl=1 68 | http://devstreaming.apple.com/videos/wwdc/2015/104usewvb5m0qbwafx8p/104/104_hd_whats_new_in_xcode.mp4?dl=1 69 | http://devstreaming.apple.com/videos/wwdc/2015/609pzlyunriyjupp/609/609_hd_deeper_into_gameplaykit_with_demobots.mp4?dl=1 70 | http://devstreaming.apple.com/videos/wwdc/2015/606ui2ppsvalj4nn/606/606_hd_enhancements_to_scenekit.mp4?dl=1 71 | http://devstreaming.apple.com/videos/wwdc/2015/6053hq2fz0ebo0lm/605/605_hd_going_social_with_replaykit_and_game_center.mp4?dl=1 72 | http://devstreaming.apple.com/videos/wwdc/2015/608rpwq1ltvg5nmk/608/608_hd_introducing_gameplaykit.mp4?dl=1 73 | http://devstreaming.apple.com/videos/wwdc/2015/602868pb0ow6idb3w/602/602_hd_managing_3d_assets_with_model_io.mp4?dl=1 74 | http://devstreaming.apple.com/videos/wwdc/2015/610kn68riy9ms89m/610/610_hd_metal_performance_optimization_techniques.mp4?dl=1 75 | http://devstreaming.apple.com/videos/wwdc/2015/6037pi9rxl6tfss8w/603/603_hd_whats_new_in_metal_part_1.mp4?dl=1 76 | http://devstreaming.apple.com/videos/wwdc/2015/607g5z16fpl7pzgi/607/607_hd_whats_new_in_metal_part_2.mp4?dl=1 77 | http://devstreaming.apple.com/videos/wwdc/2015/604gq12qghmv39znb/604/604_hd_whats_new_in_spritekit.mp4?dl=1 78 | http://devstreaming.apple.com/videos/wwdc/2015/508691kyzp/508/508_hd_audio_unit_extensions.mp4?dl=1 79 | http://devstreaming.apple.com/videos/wwdc/2015/502sufwcpog/502/502_hd_content_protection_for_http_live_streaming.mp4?dl=1 80 | http://devstreaming.apple.com/videos/wwdc/2015/5062qehwhs/506/506_hd_editing_movies_in_av_foundation.mp4?dl=1 81 | http://devstreaming.apple.com/videos/wwdc/2015/503oad8l55m/503/503_hd_monetize_and_promote_your_app_with_iad.mp4?dl=1 82 | http://devstreaming.apple.com/videos/wwdc/2015/511kmynuza/511/511_hd_safari_extensibility_content_blocking_and_shared_links.mp4?dl=1 83 | http://devstreaming.apple.com/videos/wwdc/2015/5059xl75l59/505/505_hd_using_safari_to_deliver_and_debug_a_responsive_web_design.mp4?dl=1 84 | http://devstreaming.apple.com/videos/wwdc/2015/507pq8rldk/507/507_hd_whats_new_in_core_audio.mp4?dl=1 85 | http://devstreaming.apple.com/videos/wwdc/2015/510jiccqsz/510/510_hd_whats_new_in_core_image.mp4?dl=1 86 | http://devstreaming.apple.com/videos/wwdc/2015/501g8vwlgg2/501/501_hd_whats_new_in_web_development_in_webkit_and_safari.mp4?dl=1 87 | http://devstreaming.apple.com/videos/wwdc/2015/707ysegpumy/707/707_hd_achieving_allday_battery_life.mp4?dl=1 88 | http://devstreaming.apple.com/videos/wwdc/2015/702lp563ezbr/702/702_hd_apple_pay_within_apps.mp4?dl=1 89 | http://devstreaming.apple.com/videos/wwdc/2015/718b7aw9tq/718/718_hd_building_responsive_and_efficient_apps_with_gcd.mp4?dl=1 90 | http://devstreaming.apple.com/videos/wwdc/2015/710jle9eakx/710/710_hd_cloudkit_js_and_web_services.mp4?dl=1 91 | http://devstreaming.apple.com/videos/wwdc/2015/7153rwmu2r/715/715_hd_cloudkit_tips_and_tricks.mp4?dl=1 92 | http://devstreaming.apple.com/videos/wwdc/2015/708xzs0dtwx/708/708_hd_debugging_energy_issues.mp4?dl=1 93 | http://devstreaming.apple.com/videos/wwdc/2015/709jcaer6su/709/709_hd_introducing_search_apis.mp4?dl=1 94 | http://devstreaming.apple.com/videos/wwdc/2015/713gc2tqvvb/713/713_hd_introducing_watch_connectivity.mp4?dl=1 95 | http://devstreaming.apple.com/videos/wwdc/2015/7125ovmdf36/712/712_hd_low_energy_high_performance_compression_and_accelerate.mp4?dl=1 96 | http://devstreaming.apple.com/videos/wwdc/2015/711y6zlz0ll/711/711_hd_networking_with_nsurlsession.mp4?dl=1 97 | http://devstreaming.apple.com/videos/wwdc/2015/703kp2dwbwkr/703/703_hd_privacy_and_your_app.mp4?dl=1 98 | http://devstreaming.apple.com/videos/wwdc/2015/706nu20qkag/706/706_hd_security_and_your_apps.mp4?dl=1 99 | http://devstreaming.apple.com/videos/wwdc/2015/701i2qis0reg/701/701_hd_wallet__the_home_for_apple_pay_and_more.mp4?dl=1 100 | http://devstreaming.apple.com/videos/wwdc/2015/704ci202euy/704/704_hd_whats_new_in_cloudkit.mp4?dl=1 101 | http://devstreaming.apple.com/videos/wwdc/2015/714tqy593v/714/714_hd_whats_new_in_core_location.mp4?dl=1 102 | http://devstreaming.apple.com/videos/wwdc/2015/705qrxhfxo0/705/705_hd_whats_new_in_core_motion.mp4?dl=1 103 | http://devstreaming.apple.com/videos/wwdc/2015/717yq4y85w/717/717_hd_whats_new_in_network_extension_and_vpn.mp4?dl=1 104 | http://devstreaming.apple.com/videos/wwdc/2015/720xwbi9nl/720/720_hd_whats_new_in_notifications.mp4?dl=1 105 | http://devstreaming.apple.com/videos/wwdc/2015/719ui2k57m/719/719_hd_your_app_and_next_generation_networks.mp4?dl=1 106 | -------------------------------------------------------------------------------- /WWDCSubGetter/String+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Extensions.swift 3 | 4 | // 5 | // Created by Seyed Samad Gholamzadeh on 9/9/1395 AP. 6 | // Copyright © 1395 AP Seyed Samad Gholamzadeh. All rights reserved. 7 | // 8 | 9 | import CoreGraphics 10 | import CoreText 11 | import Foundation 12 | 13 | typealias RegexPattern = String 14 | 15 | extension CGColor { 16 | public static var yellow: CGColor { 17 | return CGColor(red: 1, green: 1, blue: 0, alpha: 1) 18 | } 19 | } 20 | 21 | extension String { 22 | /* 23 | Here's a way to change it on the fly with Swift, add an extension function to String: 24 | Assuming you have the regular Localizable.strings set up with lang_id.lproj ( e.g. en.lproj, de.lproj etc. ) you can use this anywhere you need: 25 | var val = "MY_LOCALIZED_STRING".localized("de") 26 | */ 27 | func localized(lang:String) ->String { 28 | let path = Bundle.main.path(forResource: lang, ofType: "lproj") 29 | let bundle = Bundle(path: path!) 30 | return NSLocalizedString(self, tableName: nil, bundle: bundle!, value: "", comment: "") 31 | } 32 | 33 | func replace(_ letters: String, with newLetters: String) -> String { 34 | return self.replacingOccurrences(of: letters, with: newLetters, options: .literal, range: nil) 35 | } 36 | 37 | //MARK: - Regular Expression methods 38 | 39 | @available(OSX 10.12, *) 40 | func highlight(matches pattern: RegexPattern) -> NSAttributedString? { 41 | 42 | do { 43 | let regex = try NSRegularExpression(pattern: pattern, options: []) 44 | let range = NSMakeRange(0, self.count) 45 | let matches = regex.matches(in: self, options: [], range: range) 46 | let attributedText = NSMutableAttributedString(string: self) 47 | 48 | for match in matches { 49 | #if TARGET_OS_IPHONE 50 | // iOS code 51 | attributedText.addAttribute(NSBackgroundColorAttributeName, value: CGColor.yellow, range: match.range) 52 | #else 53 | // OSX code 54 | // attributedText.ad 55 | attributedText.addAttribute(NSAttributedString.Key(rawValue: kCTBackgroundColorAttributeName as String), value: CGColor.yellow, range: match.range) 56 | 57 | #endif 58 | 59 | } 60 | return attributedText.copy() as? NSAttributedString 61 | 62 | } catch let error as NSError { 63 | print(error.localizedDescription) 64 | return nil 65 | } 66 | } 67 | 68 | func list( matches pattern: RegexPattern) -> [String]? { 69 | 70 | do { 71 | let regex = try NSRegularExpression(pattern: pattern, options: []) 72 | let range = NSMakeRange(0, self.count) 73 | let matches = regex.matches(in: self, options: [], range: range) 74 | return matches.map { 75 | let range = $0.range 76 | return (self as NSString).substring(with: range) 77 | } 78 | 79 | } catch let error as NSError { 80 | print(error.localizedDescription) 81 | return nil 82 | } 83 | } 84 | 85 | func captureGroups(with pattern: RegexPattern) -> [String]? { 86 | 87 | do { 88 | let regex = try NSRegularExpression(pattern: pattern, options: []) 89 | let range = NSMakeRange(0, self.count) 90 | let matches = regex.matches(in: self, options: [], range: range) 91 | 92 | var groupMatches = [String]() 93 | for match in matches { 94 | let rangeCount = match.numberOfRanges 95 | 96 | for group in 0.. Bool { 109 | 110 | do { 111 | let regex = try NSRegularExpression(pattern: pattern, options: []) 112 | let range = NSMakeRange(0, self.count) 113 | return regex.firstMatch(in: self, options: [], range: range) != nil 114 | } catch let error as NSError { 115 | print(error.localizedDescription) 116 | return false 117 | } 118 | } 119 | 120 | func replace(matches pattern: RegexPattern, with replacementString: String) -> String? { 121 | 122 | do { 123 | let regex = try NSRegularExpression(pattern: pattern, options: []) 124 | let range = NSMakeRange(0, self.count) 125 | return regex.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: replacementString) 126 | } catch let error as NSError { 127 | print(error.localizedDescription) 128 | return nil 129 | } 130 | } 131 | 132 | 133 | 134 | } 135 | -------------------------------------------------------------------------------- /WWDCSubGetter/Subtitle.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Subtitle.swift 3 | WWDC.srt 4 | 5 | Created by Seyed Samad Gholamzadeh on 7/24/1396 AP. 6 | Copyright © 1396 AP Seyed Samad Gholamzadeh. All rights reserved. 7 | 8 | Abstract: 9 | This file contains `Webvtt` and `Subtitle` structs. 10 | */ 11 | 12 | import Foundation 13 | 14 | // MARK: - Subtitle 15 | 16 | struct Subtitle: Comparable { 17 | 18 | var id: ID { 19 | wwdcYear.id 20 | } 21 | 22 | var m3u8URL: URL { 23 | wwdcYear.m3u8URL 24 | } 25 | 26 | var videoURL: String { 27 | wwdcYear.videoURL 28 | } 29 | 30 | var webvtts: [Webvtt] { 31 | wwdcYear.webvtts 32 | } 33 | 34 | var wwdcYear: WWDCYear 35 | 36 | init?(videoURL: String, wwdc: WWDC) { 37 | 38 | let wwdcYear: WWDCYear? = { 39 | 40 | switch wwdc { 41 | case .of2024, .of2023, .of2022, .of2021: 42 | return WWDC2021(videoURL: videoURL) 43 | case .of2020: 44 | return WWDC2020(videoURL: videoURL) 45 | default: 46 | return WWDC2019(videoURL: videoURL) 47 | } 48 | }() 49 | 50 | guard let wwdcYear = wwdcYear else { 51 | return nil 52 | } 53 | 54 | self.wwdcYear = wwdcYear 55 | } 56 | 57 | mutating func updateWebvtts(with url: URL) throws { 58 | try wwdcYear.updateWebvtts(with: url) 59 | } 60 | 61 | func url(for webvtt: Webvtt) -> URL { 62 | wwdcYear.url(for: webvtt) 63 | } 64 | 65 | /** 66 | This method exports srt files with ordering webvtts in webvtts array 67 | and gatherin their contents in a single srt file. 68 | */ 69 | func exportSrtFile() { 70 | var subString: String = "" 71 | let array = self.wwdcYear.webvtts.sorted() 72 | for Webvtt in array { 73 | subString += Webvtt.content 74 | } 75 | 76 | // Some of webvtts have some joint content. we need found this joint parts and delete them. 77 | var subArray = subString.components(separatedBy: "\n\n") 78 | subArray = subArray.removingDuplicates() 79 | subString = subArray.filter{!$0.isEmpty}.map{"\(subArray.firstIndex(of: $0)! + 1)\n" + $0 }.joined(separator: "\n\n\n") + "\n\n" 80 | self.saveSrtFileAtDestination(with: subString) 81 | SubtitlesProgress.changed() 82 | } 83 | 84 | /** 85 | This method saves given text as content of exported subtitle in a srt files. 86 | 87 | - note: We save two srt file in local url for both SD and HD versions of WWDC video. 88 | 89 | - parameter text: The content that should save in srt file. 90 | */ 91 | private func saveSrtFileAtDestination(with text: String) { 92 | 93 | if let dir = model.destinationURL { 94 | 95 | //writing 96 | do { 97 | let folderPathHD = "/WWDC_\(self.wwdcYear.year)_Video_Subtitles/HD/" 98 | let folderPathSD = "/WWDC_\(self.wwdcYear.year)_Video_Subtitles/SD/" 99 | let hdPath = dir.path + folderPathHD 100 | let sdPath = dir.path + folderPathSD 101 | try FileManager.default.createDirectory(atPath: hdPath, withIntermediateDirectories: true, attributes: nil) 102 | try FileManager.default.createDirectory(atPath: sdPath, withIntermediateDirectories: true, attributes: nil) 103 | 104 | var path = dir.appendingPathComponent(folderPathHD + wwdcYear.subtitleNameForHD) 105 | try text.write(to: path, atomically: false, encoding: String.Encoding.utf8) 106 | 107 | path = dir.appendingPathComponent(folderPathSD + wwdcYear.subtitleNameForSD) 108 | try text.write(to: path, atomically: false, encoding: String.Encoding.utf8) 109 | 110 | } 111 | catch {/* error handling here */ 112 | print(error.localizedDescription) 113 | } 114 | } 115 | } 116 | 117 | mutating func appendWebvtt(_ webvtt: Webvtt) { 118 | wwdcYear.webvtts.append(webvtt) 119 | } 120 | 121 | mutating func clearWebvtts() { 122 | wwdcYear.webvtts.removeAll() 123 | } 124 | 125 | } 126 | 127 | // We need to just compare `Subtitle`s according to their id, to sort in `Subtitle` webvtts array. 128 | func <(lhs: Subtitle, rhs: Subtitle) -> Bool { 129 | return lhs.wwdcYear.id < rhs.wwdcYear.id 130 | } 131 | 132 | func ==(lhs: Subtitle, rhs: Subtitle) -> Bool { 133 | return lhs.wwdcYear.id == rhs.wwdcYear.id 134 | } 135 | 136 | 137 | /// A struct for saving and managing video link informations. 138 | struct OldSubtitle: Comparable { 139 | 140 | /// This is the pattern we use to verify valid WWDC Video links. 141 | private let pattern = "(http(?:s)?:\\/\\/devstreaming[\\S\\w]*.apple.com\\/videos\\/[wdctuiorals]+\\/)(\\d+)(\\/\\w+\\/)(\\w+)(\\/)(\\w+(?:-)?\\w+)\\.m[op4][v4](?:\\?dl\\=1)?" 142 | 143 | private let pattern2020 = "(http(?:s)?:\\/\\/devstreaming[\\S\\w]*.apple.com\\/videos\\/[wdctuiorals]+\\/)(\\d+)(\\/)(\\d+)(\\/\\d+\\/)([a-zA-Z0-9\\-]+\\/)([a-zA-Z0-9\\-\\_]+)\\.m[op4][v4](?:\\?dl\\=1)?" 144 | 145 | private let pattern2021 = "(http(?:s)?:\\/\\/devstreaming[\\S\\w]*.apple.com\\/videos\\/[wdctuiorals]+\\/)(\\d+)(\\/)(\\d+)(\\/\\d+\\/)([a-zA-Z0-9\\-]+\\/)(downloads\\/)([a-zA-Z0-9\\-\\_]+)\\.m[op4][v4](?:\\?dl\\=1)?" 146 | 147 | /// A prefix of inputed video url which used for geting m3u8 and webvtt files url 148 | private let videoURLPrefix: String 149 | 150 | private let sessionNumber: Int 151 | 152 | var webvtts: [Webvtt] = [] 153 | 154 | let wwdcYear: Int 155 | 156 | /// The given WWDC Video download url 157 | let videoURL: String 158 | 159 | /// The given WWDC Video name 160 | let videoName: String 161 | 162 | /* 163 | We export subtitles for both SD and HD version of videos. 164 | So we have subtitle name property for each of them. 165 | */ 166 | 167 | /// Subtitle name for HD version of WWDC Video 168 | private var subtitleNameForHD: String { 169 | return self.videoName + ".srt" 170 | } 171 | 172 | /// Subtitle name for SD version of WWDC Video 173 | private var subtitleNameForSD: String { 174 | return self.subtitleNameForHD.replace("_hd_", with: "_sd_").replace("-HD", with: "-SD").replace("_hd.", with: "_sd.") 175 | } 176 | 177 | /// The url that used to download subtitle m3u8 file 178 | var m3u8URL: URL { 179 | if self.wwdcYear == 2020 { 180 | return URL(string: self.videoURLPrefix + "cc/en/en.m3u8")! 181 | } 182 | return URL(string:self.videoURLPrefix + "subtitles/eng/prog_index.m3u8")! 183 | } 184 | 185 | /// The id used to save subtitle in the model 186 | var id: ID { 187 | return Int("\(wwdcYear)\(sessionNumber)")! 188 | } 189 | 190 | /// We Check validation of inputed WWDC video url and if it wasn't valid we return nil in initialization. 191 | /// - parameter videoURL: WWDC video url which want to export its subtitle. 192 | init?(videoURL: String) { 193 | let regexGroup = videoURL.captureGroups(with: pattern)! 194 | let regexGroup2020 = videoURL.captureGroups(with: pattern2020)! 195 | let regexGroup2021 = videoURL.captureGroups(with: pattern2021)! 196 | if !regexGroup.isEmpty || !regexGroup2020.isEmpty || !regexGroup2021.isEmpty { 197 | /* 198 | If video url was valid we export some information from video url 199 | like video name , wwdc year, session number,... and save them to 200 | related properties. 201 | */ 202 | var localSessionNumber = "" 203 | if !regexGroup.isEmpty { 204 | self.videoURL = regexGroup[0] 205 | self.videoURLPrefix = regexGroup[1...5].joined() 206 | self.videoName = regexGroup[6] 207 | self.wwdcYear = Int(regexGroup[2])! 208 | localSessionNumber = regexGroup[4] 209 | 210 | } else if !regexGroup2020.isEmpty { 211 | self.videoURL = regexGroup2020[0] 212 | self.videoURLPrefix = regexGroup2020[1...6].joined() 213 | self.videoName = regexGroup2020[7] 214 | self.wwdcYear = Int(regexGroup2020[2])! 215 | localSessionNumber = regexGroup2020[4] 216 | 217 | } else { 218 | self.videoURL = regexGroup2021[0] 219 | self.videoURLPrefix = regexGroup2021[1...6].joined() 220 | self.videoName = regexGroup2021[8] 221 | self.wwdcYear = Int(regexGroup2021[2])! 222 | localSessionNumber = regexGroup2021[4] 223 | } 224 | 225 | self.sessionNumber = Int(localSessionNumber) ?? { 226 | 227 | /* 228 | Some video links group 4 of pattern is'nt session number, 229 | so we should get the session number in other ways. 230 | If we failed to get session number in any way so we 231 | return a randomNumber as session number. 232 | - note: this number isn't use anywhere in exported srt file. 233 | */ 234 | func randomNumber() -> Int { 235 | return Int(arc4random_uniform(100))*Int(arc4random_uniform(101)) 236 | } 237 | 238 | if let group = regexGroup[3].captureGroups(with: "\\/(\\d+)\\w+\\/") { 239 | if let number = Int(group[1]) { 240 | return number 241 | } 242 | else { 243 | return randomNumber() 244 | } 245 | } 246 | else { 247 | return randomNumber() 248 | } 249 | }() 250 | 251 | } 252 | else { 253 | return nil 254 | } 255 | } 256 | 257 | 258 | /// This method gives url for download webvtt file related to given webvtt. 259 | func url(for webvtt: Webvtt) -> URL { 260 | if self.wwdcYear == 2020 { 261 | return URL(string: self.videoURLPrefix + "cc/en/" + webvtt.name)! 262 | } 263 | return URL(string: self.videoURLPrefix + "subtitles/eng/" + webvtt.name)! 264 | } 265 | 266 | 267 | } 268 | 269 | 270 | // We need to just compare `Subtitle`s according to their id, to sort in `Subtitle` webvtts array. 271 | func <(lhs: OldSubtitle, rhs: OldSubtitle) -> Bool { 272 | return lhs.id < rhs.id 273 | } 274 | 275 | func ==(lhs: OldSubtitle, rhs: OldSubtitle) -> Bool { 276 | return lhs.id == rhs.id 277 | } 278 | -------------------------------------------------------------------------------- /WWDCSubGetter/SubtitlesProgress.swift: -------------------------------------------------------------------------------- 1 | /* 2 | SubtitlesProgress.swift 3 | WWDC.srt 4 | 5 | Created by Seyed Samad Gholamzadeh on 7/24/1396 AP. 6 | Copyright © 1396 AP Seyed Samad Gholamzadeh. All rights reserved. 7 | 8 | Abstract: 9 | This file manages progress value of subtitle exporting for showing in `progressIndicator`. 10 | */ 11 | 12 | import Cocoa 13 | /** 14 | We create a property of this protocol in `SubtitleProgress` and conform `MainViewController to this protocol to alert it about changes of subtitle exporting progress status. 15 | */ 16 | protocol ProgressView { 17 | func progressChanged(to value: Double) 18 | } 19 | 20 | /** 21 | This struct is mediator between `Subtitle` struct and `MainViewController` 22 | to alert subtitles exporting status, and for this reason it's property and methods 23 | are static, because we need shared properties and methods for this purpose. 24 | every subtitle which wants export, calling `SubtitlesProgress.changed()` method of 25 | this struct, and this struct computes progress value of exporting entire subtitles 26 | which are in queue. 27 | */ 28 | 29 | struct SubtitlesProgress { 30 | 31 | static var min: Double = 0 32 | static var max: Double = 100 33 | static var current: Double = 0 34 | static var unit: Double = 1.0 35 | 36 | static var progressView: ProgressView? 37 | 38 | // Here we compute new value of progress and calling `ProgressView` progressChanged method. 39 | static func changed() { 40 | self.current += self.unit 41 | self.progressView?.progressChanged(to: self.current) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /WWDCSubGetter/TextFileView.swift: -------------------------------------------------------------------------------- 1 | /* 2 | TextFileView.swift 3 | WWDC.srt 4 | 5 | Created by Seyed Samad Gholamzadeh on 7/21/1396 AP. 6 | Copyright © 1396 AP Seyed Samad Gholamzadeh. All rights reserved. 7 | 8 | Abstract: 9 | This files contains method for enable drag and drop ability for TextFileView 10 | */ 11 | 12 | import Cocoa 13 | 14 | /// protocol which called when a true text file dropped to view 15 | protocol TextFileViewDelegate { 16 | 17 | func droppedTextFileURL(_ url: URL) 18 | } 19 | 20 | /// A subclass of `NSView` which have ability to accept droped files to itself. 21 | final class TextFileView: NSView { 22 | 23 | enum Appearance { 24 | static let lineWidth: CGFloat = 10.0 25 | } 26 | 27 | var delegate: TextFileViewDelegate? 28 | 29 | override func awakeFromNib() { 30 | setup() 31 | } 32 | 33 | var acceptableTypes: Set { 34 | 35 | if #available(OSX 10.13, *) { 36 | 37 | return [.URL, .fileURL] 38 | 39 | } else { 40 | 41 | return [NSPasteboard.PasteboardType(rawValue: "public.file-url")] 42 | } 43 | } 44 | 45 | func setup() { 46 | registerForDraggedTypes(Array(acceptableTypes)) 47 | } 48 | 49 | // We want just accept files that are text type 50 | lazy var filteringOptions = [NSPasteboard.ReadingOptionKey.urlReadingContentsConformToTypes:NSAttributedString.textTypes] 51 | 52 | func shouldAllowDrag(_ draggingInfo: NSDraggingInfo) -> Bool { 53 | 54 | var canAccept = false 55 | 56 | //2. 57 | let pasteBoard = draggingInfo.draggingPasteboard 58 | 59 | //3. 60 | if pasteBoard.canReadObject(forClasses: [NSURL.self], options: filteringOptions) { 61 | canAccept = true 62 | } 63 | return canAccept 64 | 65 | } 66 | 67 | override func draw(_ dirtyRect: NSRect) { 68 | 69 | if isReceivingDrag { 70 | NSColor.selectedControlColor.set() 71 | 72 | let path = NSBezierPath(rect:bounds) 73 | path.lineWidth = Appearance.lineWidth 74 | path.stroke() 75 | } 76 | } 77 | 78 | var isReceivingDrag = false { 79 | didSet { 80 | needsDisplay = true 81 | } 82 | } 83 | 84 | override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation { 85 | let allow = shouldAllowDrag(sender) 86 | isReceivingDrag = allow 87 | return allow ? .copy : NSDragOperation() 88 | } 89 | 90 | 91 | override func draggingExited(_ sender: NSDraggingInfo?) { 92 | isReceivingDrag = false 93 | } 94 | 95 | override func prepareForDragOperation(_ sender: NSDraggingInfo) -> Bool { 96 | let allow = shouldAllowDrag(sender) 97 | return allow 98 | } 99 | 100 | override func performDragOperation(_ draggingInfo: NSDraggingInfo) -> Bool { 101 | 102 | isReceivingDrag = false 103 | let pasteBoard = draggingInfo.draggingPasteboard 104 | 105 | if let urls = pasteBoard.readObjects(forClasses: [NSURL.self], options:filteringOptions) as? [URL], urls.count > 0 { 106 | delegate?.droppedTextFileURL(urls.first!) 107 | return true 108 | } 109 | return false 110 | 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /WWDCSubGetter/WWDC2019.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WWDC2019.swift 3 | // WWDC.srt 4 | // 5 | // Created by Seyed Samad Gholamzadeh on 9/14/23. 6 | // Copyright © 2023 Seyed Samad Gholamzadeh. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct WWDC2019: WWDCYear { 12 | let pattern: String = "(http(?:s)?:\\/\\/devstreaming[\\S\\w]*.apple.com\\/videos\\/[wdctuiorals]+\\/)(\\d+)(\\/\\w+\\/)(\\w+)(\\/)(\\w+(?:-)?\\w+)\\.m[op4][v4](?:\\?dl\\=1)?" 13 | 14 | let videoURLPrefix: String 15 | 16 | let sessionNumber: Int 17 | 18 | let year: String 19 | 20 | let videoURL: String 21 | 22 | let videoName: String 23 | 24 | var subtitleNameForHD: String { 25 | return self.videoName + ".srt" 26 | } 27 | 28 | var subtitleNameForSD: String { 29 | return self.subtitleNameForHD.replace("_hd_", with: "_sd_").replace("-HD", with: "-SD").replace("_hd.", with: "_sd.") 30 | } 31 | 32 | var m3u8URL: URL { 33 | return URL(string:self.videoURLPrefix + "subtitles/eng/prog_index.m3u8")! 34 | } 35 | 36 | var id: ID { 37 | return Int("\(year)\(sessionNumber)")! 38 | } 39 | 40 | func url(for webvtt: Webvtt) -> URL { 41 | return URL(string: self.videoURLPrefix + "subtitles/eng/" + webvtt.name)! 42 | } 43 | 44 | var webvtts: [Webvtt] = [] 45 | 46 | init?(videoURL: String) { 47 | let regexGroup = videoURL.captureGroups(with: pattern)! 48 | if !regexGroup.isEmpty { 49 | /* 50 | If video url was valid we export some information from video url 51 | like video name , wwdc year, session number,... and save them to 52 | related properties. 53 | */ 54 | 55 | self.videoURL = regexGroup[0] 56 | self.videoURLPrefix = regexGroup[1...5].joined() 57 | self.videoName = regexGroup[6] 58 | self.year = String(regexGroup[2]) 59 | let localSessionNumber = regexGroup[4] 60 | 61 | self.sessionNumber = Int(localSessionNumber) ?? { 62 | 63 | /* 64 | Some video links group 4 of pattern is'nt session number, 65 | so we should get the session number in other ways. 66 | If we failed to get session number in any way so we 67 | return a randomNumber as session number. 68 | - note: this number isn't use anywhere in exported srt file. 69 | */ 70 | func randomNumber() -> Int { 71 | return Int(arc4random_uniform(100))*Int(arc4random_uniform(101)) 72 | } 73 | 74 | if let group = regexGroup[3].captureGroups(with: "\\/(\\d+)\\w+\\/") { 75 | if let number = Int(group[1]) { 76 | return number 77 | } 78 | else { 79 | return randomNumber() 80 | } 81 | } 82 | else { 83 | return randomNumber() 84 | } 85 | }() 86 | 87 | } 88 | else { 89 | return nil 90 | } 91 | } 92 | 93 | 94 | /* 95 | var subsURLArray = string.components(separatedBy: "\n").filter {$0.contains("fileSequence")} 96 | if self.subtitle.wwdcYear >= 2021 { 97 | subsURLArray = string.components(separatedBy: "\n").filter {$0.contains("sequence")} 98 | } 99 | */ 100 | mutating func updateWebvtts(with url: URL) throws { 101 | /* 102 | We convert m3u8 file to an arry of `Webvtt` objects, 103 | which are the original subtitle files. 104 | We download and convert this webvtt files in a single srt file later. 105 | */ 106 | let string = try String(contentsOf: url, encoding: String.Encoding.utf8) 107 | let subsURLArray = string.components(separatedBy: "\n").filter {$0.contains("fileSequence")} 108 | 109 | 110 | let webvttArray = subsURLArray.map {Webvtt(number: subsURLArray.firstIndex(of: $0)!, content: "", name: $0)} 111 | self.webvtts = webvttArray 112 | } 113 | 114 | 115 | } 116 | -------------------------------------------------------------------------------- /WWDCSubGetter/WWDC2020.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WWDC2020.swift 3 | // WWDC.srt 4 | // 5 | // Created by Seyed Samad Gholamzadeh on 9/14/23. 6 | // Copyright © 2023 Seyed Samad Gholamzadeh. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct WWDC2020: WWDCYear { 12 | let pattern: String = "(http(?:s)?:\\/\\/devstreaming[\\S\\w]*.apple.com\\/videos\\/[wdctuiorals]+\\/)(\\d+)(\\/)(\\d+)(\\/\\d+\\/)([a-zA-Z0-9\\-]+\\/)([a-zA-Z0-9\\-\\_]+)\\.m[op4][v4](?:\\?dl\\=1)?" 13 | 14 | let videoURLPrefix: String 15 | 16 | let sessionNumber: Int 17 | 18 | let year: String 19 | 20 | let videoURL: String 21 | 22 | let videoName: String 23 | 24 | var subtitleNameForHD: String { 25 | return self.videoName + ".srt" 26 | } 27 | 28 | var subtitleNameForSD: String { 29 | return self.subtitleNameForHD.replace("_hd.", with: "_sd.") 30 | } 31 | 32 | var m3u8URL: URL { 33 | return URL(string: self.videoURLPrefix + "cc/en/en.m3u8")! 34 | } 35 | 36 | var id: ID { 37 | return Int("\(year)\(sessionNumber)")! 38 | } 39 | 40 | func url(for webvtt: Webvtt) -> URL { 41 | return URL(string: self.videoURLPrefix + "cc/en/" + webvtt.name)! 42 | } 43 | 44 | var webvtts: [Webvtt] = [] 45 | 46 | init?(videoURL: String) { 47 | let regexGroup = videoURL.captureGroups(with: pattern)! 48 | if !regexGroup.isEmpty { 49 | /* 50 | If video url was valid we export some information from video url 51 | like video name , wwdc year, session number,... and save them to 52 | related properties. 53 | */ 54 | 55 | self.videoURL = regexGroup[0] 56 | self.videoURLPrefix = regexGroup[1...6].joined() 57 | self.videoName = regexGroup[7] 58 | self.year = String(regexGroup[2]) 59 | let localSessionNumber = regexGroup[4] 60 | 61 | self.sessionNumber = Int(localSessionNumber) ?? { 62 | 63 | /* 64 | Some video links group 4 of pattern is'nt session number, 65 | so we should get the session number in other ways. 66 | If we failed to get session number in any way so we 67 | return a randomNumber as session number. 68 | - note: this number isn't use anywhere in exported srt file. 69 | */ 70 | func randomNumber() -> Int { 71 | return Int(arc4random_uniform(100))*Int(arc4random_uniform(101)) 72 | } 73 | 74 | if let group = regexGroup[3].captureGroups(with: "\\/(\\d+)\\w+\\/") { 75 | if let number = Int(group[1]) { 76 | return number 77 | } 78 | else { 79 | return randomNumber() 80 | } 81 | } 82 | else { 83 | return randomNumber() 84 | } 85 | }() 86 | 87 | } 88 | else { 89 | return nil 90 | } 91 | } 92 | 93 | 94 | /* 95 | var subsURLArray = string.components(separatedBy: "\n").filter {$0.contains("fileSequence")} 96 | if self.subtitle.wwdcYear >= 2021 { 97 | subsURLArray = string.components(separatedBy: "\n").filter {$0.contains("sequence")} 98 | } 99 | */ 100 | mutating func updateWebvtts(with url: URL) throws { 101 | /* 102 | We convert m3u8 file to an arry of `Webvtt` objects, 103 | which are the original subtitle files. 104 | We download and convert this webvtt files in a single srt file later. 105 | */ 106 | let string = try String(contentsOf: url, encoding: String.Encoding.utf8) 107 | let subsURLArray = string.components(separatedBy: "\n").filter {$0.contains("fileSequence")} 108 | 109 | 110 | let webvttArray = subsURLArray.map {Webvtt(number: subsURLArray.firstIndex(of: $0)!, content: "", name: $0)} 111 | self.webvtts = webvttArray 112 | } 113 | 114 | 115 | } 116 | -------------------------------------------------------------------------------- /WWDCSubGetter/WWDC2021.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WWDC2021.swift 3 | // WWDC.srt 4 | // 5 | // Created by Seyed Samad Gholamzadeh on 9/14/23. 6 | // Copyright © 2023 Seyed Samad Gholamzadeh. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct WWDC2021: WWDCYear { 12 | let pattern: String = "(http(?:s)?:\\/\\/devstreaming[\\S\\w]*.apple.com\\/videos\\/[wdctuiorals]+\\/)(\\d+)(\\/)(\\d+)(\\/\\d+\\/)([a-zA-Z0-9\\-]+\\/)(downloads\\/)([a-zA-Z0-9\\-\\_]+)\\.m[op4][v4](?:\\?dl\\=1)?" 13 | 14 | let videoURLPrefix: String 15 | 16 | let sessionNumber: Int 17 | 18 | let year: String 19 | 20 | let videoURL: String 21 | 22 | let videoName: String 23 | 24 | var subtitleNameForHD: String { 25 | return self.videoName + ".srt" 26 | } 27 | 28 | var subtitleNameForSD: String { 29 | return self.subtitleNameForHD.replace("_hd.", with: "_sd.") 30 | } 31 | 32 | var m3u8URL: URL { 33 | URL(string:self.videoURLPrefix + "subtitles/eng/prog_index.m3u8")! 34 | } 35 | 36 | var id: ID { 37 | return Int("\(year)\(sessionNumber)")! 38 | } 39 | 40 | func url(for webvtt: Webvtt) -> URL { 41 | return URL(string: self.videoURLPrefix + "subtitles/eng/" + webvtt.name)! 42 | } 43 | 44 | var webvtts: [Webvtt] = [] 45 | 46 | init?(videoURL: String) { 47 | let regexGroup = videoURL.captureGroups(with: pattern)! 48 | if !regexGroup.isEmpty { 49 | /* 50 | If video url was valid we export some information from video url 51 | like video name , wwdc year, session number,... and save them to 52 | related properties. 53 | */ 54 | 55 | self.videoURL = regexGroup[0] 56 | self.videoURLPrefix = regexGroup[1...6].joined() 57 | self.videoName = regexGroup[8] 58 | self.year = String(regexGroup[2]) 59 | let localSessionNumber = regexGroup[4] 60 | 61 | self.sessionNumber = Int(localSessionNumber) ?? { 62 | 63 | /* 64 | Some video links group 4 of pattern is'nt session number, 65 | so we should get the session number in other ways. 66 | If we failed to get session number in any way so we 67 | return a randomNumber as session number. 68 | - note: this number isn't use anywhere in exported srt file. 69 | */ 70 | func randomNumber() -> Int { 71 | return Int(arc4random_uniform(100))*Int(arc4random_uniform(101)) 72 | } 73 | 74 | if let group = regexGroup[3].captureGroups(with: "\\/(\\d+)\\w+\\/") { 75 | if let number = Int(group[1]) { 76 | return number 77 | } 78 | else { 79 | return randomNumber() 80 | } 81 | } 82 | else { 83 | return randomNumber() 84 | } 85 | }() 86 | 87 | } 88 | else { 89 | return nil 90 | } 91 | } 92 | 93 | 94 | /* 95 | var subsURLArray = string.components(separatedBy: "\n").filter {$0.contains("fileSequence")} 96 | if self.subtitle.wwdcYear >= 2021 { 97 | subsURLArray = string.components(separatedBy: "\n").filter {$0.contains("sequence")} 98 | } 99 | */ 100 | mutating func updateWebvtts(with url: URL) throws { 101 | /* 102 | We convert m3u8 file to an arry of `Webvtt` objects, 103 | which are the original subtitle files. 104 | We download and convert this webvtt files in a single srt file later. 105 | */ 106 | let string = try String(contentsOf: url, encoding: String.Encoding.utf8) 107 | // webvtt pattern before 2021 108 | let subsURLArray = string.components(separatedBy: "\n").filter {$0.contains("sequence")} 109 | 110 | 111 | let webvttArray = subsURLArray.map {Webvtt(number: subsURLArray.firstIndex(of: $0)!, content: "", name: $0)} 112 | self.webvtts = webvttArray 113 | } 114 | 115 | 116 | } 117 | -------------------------------------------------------------------------------- /WWDCSubGetter/WWDCVideosController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WWDCVideosController.swift 3 | // WWDC.srt 4 | // 5 | // Created by Seyed Samad Gholamzadeh on 6/5/18. 6 | // Copyright © 2018 Seyed Samad Gholamzadeh. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SystemConfiguration 11 | 12 | 13 | enum VideoQuality: String { 14 | case hd = "hd" 15 | case sd = "sd" 16 | } 17 | 18 | 19 | class WWDCVideosController { 20 | 21 | class func getSessionsList(fromHTML: String, wwdcYear: String) -> [String] { 22 | 23 | let nsFromHTML = fromHTML as NSString 24 | // let pat = "video-title\\\\\"\\>(.*?)\\<\\/h\\d\\>" 25 | let pat = "href=\"\\/videos\\/play\\/[^ ]*?\\/([0-9]*)\\/\".*?video-title\"\\>(.*?)\\<\\/h\\d\\>" 26 | 27 | let regex = try! NSRegularExpression(pattern: pat, options: NSRegularExpression.Options.dotMatchesLineSeparators) 28 | let matches = regex.matches(in: fromHTML, options: [], range: NSRange(location: 0, length: fromHTML.count)) 29 | 30 | var sessionsListArray = [String]() 31 | 32 | 33 | matches.forEach { (match) in 34 | let numRange = match.range(at: 1) 35 | let titleRange = match.range(at: 2) 36 | let sessionNumber = nsFromHTML.substring(with: numRange) + " _ " + nsFromHTML.substring(with: titleRange) 37 | 38 | sessionsListArray.append(sessionNumber) 39 | 40 | } 41 | 42 | return sessionsListArray 43 | } 44 | 45 | class func getHDorSDdURLs(fromHTML: String, format: VideoQuality) -> (String) { 46 | 47 | let formatValue = format == .hd ? "[hH][dD]" : "[sS][dD]" 48 | 49 | // let pat = "\\b.*?(http(?:s)?://.*?" + formatValue + ".*?\\.m[op4][v4a])\\b" 50 | let pat = "(http(?:s)?[^ ]*?" + formatValue + "[^ ]*?)\\?dl" 51 | // let pat = "(http(?:s)?[^ ]*?" + formatValue + "[^ ]*?\\.(?:mp4|m4a|mov))" 52 | let regex = try! NSRegularExpression(pattern: pat, options: []) 53 | let matches = regex.matches(in: fromHTML, options: [], range: NSRange(location: 0, length: fromHTML.count)) 54 | 55 | var videoURL = "" 56 | if !matches.isEmpty { 57 | let range = matches[0].range(at: 1) 58 | videoURL = (fromHTML as NSString).substring(with: range) 59 | } 60 | 61 | return videoURL 62 | } 63 | 64 | class func getPDFResourceURL(fromHTML: String) -> [String] { 65 | // let pat = "\\b.*(http(?:s)?://.*\\.pdf)\\b" 66 | let pat = "(http(?:s)?[^ ]*?\\.pdf)\\?dl" 67 | 68 | let regex = try! NSRegularExpression(pattern: pat, options: []) 69 | let matches = regex.matches(in: fromHTML, options: [], range: NSRange(location: 0, length: fromHTML.count)) 70 | // var pdfResourceURL = "" 71 | // if !matches.isEmpty { 72 | // let range = matches[0].range(at: 1) 73 | // let r = fromHTML.index(fromHTML.startIndex, offsetBy: range.location) ..< 74 | // fromHTML.index(fromHTML.startIndex, offsetBy: range.location+range.length) 75 | // pdfResourceURL = String(fromHTML[r]) 76 | // } 77 | // 78 | // return pdfResourceURL 79 | 80 | var pdfResourceURLPaths : [String] = [] 81 | for match in matches { 82 | let range = match.range(at: 1) 83 | let path = (fromHTML as NSString).substring(with: range) 84 | pdfResourceURLPaths.append(path) 85 | } 86 | 87 | return pdfResourceURLPaths 88 | } 89 | 90 | class func getSampleCodeURL(fromHTML: String) -> [String] { 91 | let pat = "(href=\"[^ ]*?/content/samplecode/.*?=\")" 92 | let regex = try! NSRegularExpression(pattern: pat, options: []) 93 | let matches = regex.matches(in: fromHTML, options: [], range: NSRange(location: 0, length: fromHTML.count)) 94 | var sampleURLPaths : [String] = [] 95 | for match in matches { 96 | let range = match.range(at: 1) 97 | var path = (fromHTML as NSString).substring(with: range) 98 | 99 | // Tack on the hostname if it's not already there (some URLs are listed as 100 | // relative URL while some are fully-qualified). 101 | let prefixReplacementString: String 102 | if !path.contains("href=\"http") { 103 | prefixReplacementString = "http(?:s)?://developer.apple.com" 104 | } else { 105 | prefixReplacementString = "" 106 | } 107 | path = path.replacingOccurrences(of: "href=\"", with: prefixReplacementString) 108 | 109 | // Strip target attribute suffix 110 | path = path.replacingOccurrences(of: "\" target=\"", with: "/") 111 | 112 | sampleURLPaths.append(path) 113 | } 114 | 115 | var sampleArchivePaths : [String] = [] 116 | for urlPath in sampleURLPaths { 117 | let jsonText = getStringContent(fromURL: urlPath + "book.json") 118 | if let data = jsonText.data(using: .utf8) { 119 | let object = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) 120 | if let dictionary = object as? [String:Any] { 121 | if let relativePath = dictionary["sampleCode"] as? String { 122 | sampleArchivePaths.append(urlPath + relativePath) 123 | } 124 | } 125 | } 126 | } 127 | 128 | return sampleArchivePaths 129 | } 130 | 131 | class func getSampleCodeURL2(fromHTML: String) -> [String] { 132 | // let pat = "\\b.*(http(?:s)?://.*\\.zip)\\b" 133 | let pat = "(http(?:s)?[^ ]*?\\.zip)" 134 | let regex = try! NSRegularExpression(pattern: pat, options: []) 135 | let matches = regex.matches(in: fromHTML, options: [], range: NSRange(location: 0, length: fromHTML.count)) 136 | 137 | var sampleURLPaths : [String] = [] 138 | for match in matches { 139 | let range = match.range(at: 1) 140 | let path = (fromHTML as NSString).substring(with: range) 141 | sampleURLPaths.append(path) 142 | } 143 | 144 | return sampleURLPaths 145 | } 146 | 147 | class func getSampleCodeURL3(fromHTML: String) -> [String] { 148 | // let pat = "(href=\"[^ ]*?/content/samplecode/.*?=\")" 149 | let pat = "href=\"([^ ]*?/documentation/.*?)\"" 150 | let regex = try! NSRegularExpression(pattern: pat, options: []) 151 | let matches = regex.matches(in: fromHTML, options: [], range: NSRange(location: 0, length: fromHTML.count)) 152 | var sampleURLPaths : [String] = [] 153 | for match in matches { 154 | let range = match.range(at: 1) 155 | var path = (fromHTML as NSString).substring(with: range) 156 | 157 | // Tack on the hostname if it's not already there (some URLs are listed as 158 | // relative URL while some are fully-qualified). 159 | let prefixReplacementString: String 160 | if !path.contains("href=\"http") { 161 | prefixReplacementString = "http(?:s)?://developer.apple.com" 162 | } else { 163 | prefixReplacementString = "" 164 | } 165 | path = path.replacingOccurrences(of: "href=\"", with: prefixReplacementString) 166 | 167 | // Strip target attribute suffix 168 | path = path.replacingOccurrences(of: "\" target=\"", with: "/") 169 | 170 | sampleURLPaths.append(path) 171 | } 172 | 173 | var sampleArchivePaths : [String] = [] 174 | for urlPath in sampleURLPaths { 175 | let htmlText = getStringContent(fromURL: urlPath) 176 | 177 | let pat = "href=\"([^ ]*?/published/.*?.zip)\"" 178 | let regex = try! NSRegularExpression(pattern: pat, options: []) 179 | let matches = regex.matches(in: htmlText, options: [], range: NSRange(location: 0, length: htmlText.count)) 180 | for match in matches { 181 | let range = match.range(at: 1) 182 | let path = (htmlText as NSString).substring(with: range) 183 | 184 | sampleArchivePaths.append(path) 185 | } 186 | 187 | } 188 | 189 | return sampleArchivePaths 190 | } 191 | 192 | 193 | class func getStringContent(fromURL: String) -> (String) { 194 | /* Configure session, choose between: 195 | * defaultSessionConfiguration 196 | * ephemeralSessionConfiguration 197 | * backgroundSessionConfigurationWithIdentifier: 198 | And set session-wide properties, such as: HTTPAdditionalHeaders, 199 | HTTPCookieAcceptPolicy, requestCachePolicy or timeoutIntervalForRequest. 200 | */ 201 | 202 | /* Create session, and optionally set a URLSessionDelegate. */ 203 | let session = URLSession(configuration: URLSessionConfiguration.default, delegate: nil, delegateQueue: nil) 204 | 205 | /* Create the Request: 206 | My API (2) (GET https://developer.apple.com/videos/play/wwdc2017/201/) 207 | https://developer.apple.com/videos/play/wwdc2017/102/ 208 | */ 209 | var result = "" 210 | guard let URL = URL(string: fromURL) else {return result} 211 | var request = URLRequest(url: URL) 212 | request.httpMethod = "GET" 213 | 214 | /* Start a new Task */ 215 | let semaphore = DispatchSemaphore.init(value: 0) 216 | let task = session.dataTask(with: request, completionHandler: { (data: Data?, response: URLResponse?, error: Error?) -> Void in 217 | if (error == nil) { 218 | /* Success */ 219 | // let statusCode = (response as! NSHTTPURLResponse).statusCode 220 | // print("URL Session Task Succeeded: HTTP \(statusCode)") 221 | if let data = data, let string = String.init(data: data, encoding: 222 | .utf8) { 223 | result = string 224 | 225 | } 226 | } 227 | else { 228 | /* Failure */ 229 | print("URL Session Task Failed: %@", error!.localizedDescription); 230 | } 231 | 232 | semaphore.signal() 233 | }) 234 | task.resume() 235 | semaphore.wait() 236 | return result 237 | } 238 | 239 | class func downloadFile(urlString: String, forSession sessionIdentifier: String = "???") { 240 | var fileName = URL(fileURLWithPath: urlString).lastPathComponent 241 | 242 | if fileName.hasPrefix(sessionIdentifier) == false { 243 | fileName = "\(sessionIdentifier)_\(fileName)" 244 | } 245 | 246 | guard !FileManager.default.fileExists(atPath: "./" + fileName) else { 247 | print("\(fileName): already exists, nothing to do!") 248 | return 249 | } 250 | 251 | print("[Session \(sessionIdentifier)] Getting \(fileName) (\(urlString)):") 252 | 253 | guard URL(string: urlString) != nil else { 254 | print("<\(urlString)> is not valid URL!") 255 | return 256 | } 257 | 258 | // DownloadSessionManager.sharedInstance.downloadFile(fromURL: url, toPath: "\(fileName)") 259 | } 260 | 261 | } 262 | 263 | 264 | class Reachability { 265 | class func isConnectedToNetwork() -> Bool { 266 | guard let flags = getFlags() else { return false } 267 | let isReachable = flags.contains(.reachable) 268 | let needsConnection = flags.contains(.connectionRequired) 269 | return (isReachable && !needsConnection) 270 | } 271 | 272 | class func getFlags() -> SCNetworkReachabilityFlags? { 273 | guard let reachability = ipv4Reachability() ?? ipv6Reachability() else { 274 | return nil 275 | } 276 | var flags = SCNetworkReachabilityFlags() 277 | if !SCNetworkReachabilityGetFlags(reachability, &flags) { 278 | return nil 279 | } 280 | return flags 281 | } 282 | 283 | class func ipv6Reachability() -> SCNetworkReachability? { 284 | var zeroAddress = sockaddr_in6() 285 | zeroAddress.sin6_len = UInt8(MemoryLayout.size) 286 | zeroAddress.sin6_family = sa_family_t(AF_INET6) 287 | 288 | return withUnsafePointer(to: &zeroAddress, { 289 | $0.withMemoryRebound(to: sockaddr.self, capacity: 1) { 290 | SCNetworkReachabilityCreateWithAddress(nil, $0) 291 | } 292 | }) 293 | } 294 | 295 | class func ipv4Reachability() -> SCNetworkReachability? { 296 | var zeroAddress = sockaddr_in() 297 | zeroAddress.sin_len = UInt8(MemoryLayout.size) 298 | zeroAddress.sin_family = sa_family_t(AF_INET) 299 | 300 | return withUnsafePointer(to: &zeroAddress, { 301 | $0.withMemoryRebound(to: sockaddr.self, capacity: 1) { 302 | SCNetworkReachabilityCreateWithAddress(nil, $0) 303 | } 304 | }) 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /WWDCSubGetter/WWDCYear.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WWDCYear.swift 3 | // WWDC.srt 4 | // 5 | // Created by Seyed Samad Gholamzadeh on 9/14/23. 6 | // Copyright © 2023 Seyed Samad Gholamzadeh. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol WWDCYear { 12 | 13 | var pattern: String { get } 14 | 15 | var webvtts: [Webvtt] { get set } 16 | 17 | var videoURLPrefix: String { get } 18 | 19 | var sessionNumber: Int { get } 20 | 21 | var year: String { get } 22 | 23 | var videoURL: String { get } 24 | 25 | var videoName: String { get } 26 | 27 | var subtitleNameForHD: String { get } 28 | 29 | var subtitleNameForSD: String { get } 30 | 31 | var m3u8URL: URL { get } 32 | 33 | var id: ID { get } 34 | 35 | func url(for webvtt: Webvtt) -> URL 36 | 37 | mutating func updateWebvtts(with url: URL) throws 38 | } 39 | -------------------------------------------------------------------------------- /WWDCSubGetter/Webvtt.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Webvtt.swift 3 | // WWDC.srt 4 | // 5 | // Created by Seyed Samad Gholamzadeh on 9/14/23. 6 | // Copyright © 2023 Seyed Samad Gholamzadeh. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - Webvtt 12 | 13 | /** 14 | A struct for saving webvtt file informations, such as `name` and `number` 15 | of it in m3u8 file and the webvtt file content(after converting to srt stadard content). 16 | */ 17 | struct Webvtt: Comparable { 18 | 19 | /// number of webvtt file in m3u8 file 20 | var number: Int 21 | 22 | /// content of webvtt file 23 | var content: String 24 | 25 | /// name of webvtt file in m3u8 file 26 | var name: String 27 | } 28 | 29 | // We need to just compare `Webvtt`s according to their number, to sort in `Subtitle` webvtts array. 30 | func <(lhs: Webvtt, rhs: Webvtt) -> Bool { 31 | return lhs.number < rhs.number 32 | } 33 | 34 | func ==(lhs: Webvtt, rhs: Webvtt) -> Bool { 35 | return lhs.number == rhs.number 36 | } 37 | --------------------------------------------------------------------------------