added iOS source code
[wl-app.git] / iOS / WolneLektury / Connection / DownloadManager.swift
1 //
2 //  DownloadManager.swift
3 //  WolneLektury
4 //
5 //  Created by Pawel Dabrowski on 22/06/2018.
6 //  Copyright © 2018 Fundacja Nowoczesna Polska. All rights reserved.
7 //
8
9 import UIKit
10 import MZDownloadManager
11
12 enum FileStatus{
13     case notDownloaded
14     case downloading
15     case downloaded
16 }
17
18 enum FileType{
19     case ebook
20     case audiobook
21     
22     private var destinationPath: String{
23         switch self {
24         case .ebook:
25             return Constants.ebookPath
26         case .audiobook:
27             return Constants.audiobookPath
28         }
29     }
30     
31     func destinationPathWithSlug(bookSlug: String) -> String {
32         return destinationPath + "/" + bookSlug + "/"
33     }
34     
35     func pathForFileName(filename: String, bookSlug: String) -> String{
36         return self.destinationPathWithSlug(bookSlug: bookSlug) + filename
37     }
38 }
39
40 protocol DownloadManagerDelegate: class{
41     func downloadManagerDownloadProgressChanged(model: MZDownloadModel, allProgress: Float, bookSlug: String)
42     func downloadManagerDownloadFinished(model: MZDownloadModel, bookSlug: String)
43     func downloadManagerDownloadFailed(model: MZDownloadModel, bookSlug: String)
44 }
45
46 extension MZDownloadModel {
47     func isAudiobook() -> Bool {
48         return self.destinationPath.starts(with: Constants.audiobookPath)
49     }
50     func isEbook() -> Bool {
51         return self.destinationPath.starts(with: Constants.ebookPath)
52     }
53
54 }
55
56 class DownloadingAudiobook{
57     var allUrlsCount: Float = 0
58     var waitingToDownloadUrls: [String]
59     var downloadedUrls: [String]
60     var bookSlug: String
61     init(bookDetailsModel: BookDetailsModel) {
62         
63         waitingToDownloadUrls = [String]()
64         downloadedUrls = [String]()
65         
66         let audiobookUrls = bookDetailsModel.getAudiobookFilesUrls()
67         bookSlug = bookDetailsModel.slug
68         allUrlsCount = Float(audiobookUrls.count)
69         
70         for url in audiobookUrls{
71             if NSObject.audiobookExists(audioBookUrlString: url, bookSlug: bookSlug){
72                 downloadedUrls.append(url)
73             }
74             else{
75                 waitingToDownloadUrls.append(url)
76             }
77         }
78     }
79     
80     func getProgress() -> Float{
81         let allCount = waitingToDownloadUrls.count + downloadedUrls.count
82         if allCount > 0{
83             if downloadedUrls.count > 0{
84                 return Float(downloadedUrls.count) / Float(allCount)
85             }
86         }
87         return 0.0
88     }
89 }
90
91 class DownloadManager: NSObject, MZDownloadManagerDelegate{
92     
93     weak var delegate: DownloadManagerDelegate?
94     var downloadingAudiobooks = [DownloadingAudiobook]()
95     
96     //Shared instance of DownloadManager
97     static let sharedInstance : DownloadManager = {
98         return DownloadManager()
99     }()
100     
101     
102     lazy var downloadManager: MZDownloadManager = {
103         [unowned self] in
104         let sessionIdentifer: String = "com.moiseum.WolneLektury.BackgroundSession"
105         
106         let appDelegate = UIApplication.shared.delegate as! AppDelegate
107         var completion = appDelegate.backgroundSessionCompletionHandler
108         
109         let downloadmanager = MZDownloadManager(session: sessionIdentifer, delegate: self, completion: completion)
110         return downloadmanager
111         }()
112     
113     func checkEbookStatus(bookSlug: String) -> FileStatus{
114         if getEbookProgress(bookSlug: bookSlug) != nil{
115             return .downloading
116         }
117         
118         if ebookExists(bookSlug: bookSlug){
119             return .downloaded
120         }
121         
122         return .notDownloaded
123     }
124     
125     func getDownloadingAudiobook(bookSlug: String) -> DownloadingAudiobook? {
126         for downloadingAudiobook in downloadingAudiobooks {
127             if downloadingAudiobook.bookSlug == bookSlug {
128                 return downloadingAudiobook
129             }
130         }
131         return nil
132     }
133     
134     func clearDownloadingAudiobookFromQueue(bookSlug: String){
135         
136         var i = 0
137         var found = false
138         for downloadingAudiobook in downloadingAudiobooks {
139             if downloadingAudiobook.bookSlug == bookSlug {
140                 found = true
141                 break
142             }
143             
144             i += 1
145         }
146         if found {
147             downloadingAudiobooks.remove(at: i)
148         }
149         
150         if let index = downloadManager.downloadingArray.index(where: {$0.destinationPath == FileType.audiobook.destinationPathWithSlug(bookSlug: bookSlug)}) {
151             downloadManager.cancelTaskAtIndex(index)
152         }
153     }
154     
155     func checkAudiobookStatus(bookDetailsModel: BookDetailsModel) -> FileStatus{
156         
157         guard bookDetailsModel.slug.count > 0 else { return .notDownloaded}
158         
159         if getDownloadingAudiobook(bookSlug: bookDetailsModel.slug) != nil {
160             return .downloading
161         }
162         
163         if bookDetailsModel.checkIfAllAudiobookFilesAreDownloaded() {
164             return .downloaded
165         }
166         
167         return .notDownloaded
168     }
169
170     func deleteEbook(bookSlug: String){
171         
172         let fileType = FileType.ebook
173         
174         if let index = downloadManager.downloadingArray.index(where: {$0.fileName == bookSlug && $0.destinationPath == fileType.destinationPathWithSlug(bookSlug: bookSlug)}) {
175             downloadManager.cancelTaskAtIndex(index)
176         }
177         else {
178             let path = fileType.destinationPathWithSlug(bookSlug: bookSlug)// pathForFileName(filename: bookSlug,bookSlug: bookSlug)
179             if FileManager.default.fileExists(atPath: path) {
180                 try! FileManager.default.removeItem(atPath: path)
181             }
182         }
183         UserDefaults.standard.removeObject(forKey: bookSlug)
184     }
185     
186     func deleteAudiobook(bookSlug: String){
187         
188         let fileType = FileType.audiobook
189         
190         try? FileManager.default.removeItem(atPath: fileType.destinationPathWithSlug(bookSlug: bookSlug))
191     }
192     
193     func getEbookProgress(bookSlug: String) -> Float? {
194         if let model = downloadManager.downloadingArray.first(where: {$0.fileName == bookSlug && $0.destinationPath == FileType.ebook.destinationPathWithSlug(bookSlug: bookSlug)}){
195             return model.progress
196         }
197         return nil
198     }
199     
200     func getAudiobookProgress(bookDetailsModel: BookDetailsModel) -> Float? {
201         
202         guard let downloadingAudiobook = getDownloadingAudiobook(bookSlug: bookDetailsModel.slug) else {return nil}
203         
204         return downloadingAudiobook.getProgress()
205     }
206
207     func downloadEbook(bookDetailsModel: BookDetailsModel) {
208         
209         guard bookDetailsModel.epub.count > 0, bookDetailsModel.slug.count > 0 else {
210             return
211         }
212         
213         downloadManager.addDownloadTask(bookDetailsModel.slug, fileURL: bookDetailsModel.epub, destinationPath: FileType.ebook.destinationPathWithSlug(bookSlug: bookDetailsModel.slug))
214     }
215     
216     func downloadAudiobooks(bookDetailsModel: BookDetailsModel) {
217         
218         let bookSlug = bookDetailsModel.slug
219         guard bookDetailsModel.getAudiobookFilesUrls().count > 0, bookSlug.count > 0 else {
220             return
221         }
222         
223         if let downloadingAudiobook = getDownloadingAudiobook(bookSlug:bookSlug){
224             
225             if let firstUrl = downloadingAudiobook.waitingToDownloadUrls.first{
226                 addDownloadAudiobookTask(bookSlug: bookSlug, fileUrl: firstUrl)
227                 return
228             }
229             else{
230                 clearDownloadingAudiobookFromQueue(bookSlug: bookSlug)
231             }
232         }
233         
234         let downloadingAudiobook = DownloadingAudiobook(bookDetailsModel: bookDetailsModel)
235         if let firstUrl = downloadingAudiobook.waitingToDownloadUrls.first {
236             downloadingAudiobooks.append(downloadingAudiobook)
237             addDownloadAudiobookTask(bookSlug: bookSlug, fileUrl: firstUrl)
238         }
239     }
240     
241     func addDownloadAudiobookTask(bookSlug: String, fileUrl: String){
242         downloadManager.addDownloadTask((fileUrl as NSString).lastPathComponent, fileURL: fileUrl, destinationPath: FileType.audiobook.destinationPathWithSlug(bookSlug: bookSlug))
243     }
244
245     func notifyDelegateThatProgressChanged(downloadModel: MZDownloadModel){
246         guard let delegate = delegate else { return }
247         
248         if downloadModel.isAudiobook(){
249             for downloadAudiobook in downloadingAudiobooks {
250                 if downloadAudiobook.waitingToDownloadUrls.index(of: downloadModel.fileURL) != nil {
251                     delegate.downloadManagerDownloadProgressChanged(model: downloadModel, allProgress: downloadAudiobook.getProgress() + downloadModel.progress/downloadAudiobook.allUrlsCount, bookSlug: downloadAudiobook.bookSlug)
252                     return
253                 }
254             }
255         }
256         delegate.downloadManagerDownloadProgressChanged(model: downloadModel, allProgress: downloadModel.progress, bookSlug: downloadModel.fileName)
257     }
258     
259     func downloadRequestStarted(_ downloadModel: MZDownloadModel, index: Int) {
260         
261         notifyDelegateThatProgressChanged(downloadModel: downloadModel)
262     }
263     
264     func downloadRequestDidPopulatedInterruptedTasks(_ downloadModels: [MZDownloadModel]) {
265     }
266     
267     func downloadRequestDidUpdateProgress(_ downloadModel: MZDownloadModel, index: Int) {
268         notifyDelegateThatProgressChanged(downloadModel: downloadModel)
269     }
270     
271     func downloadRequestDidPaused(_ downloadModel: MZDownloadModel, index: Int) {
272     }
273     
274     func downloadRequestDidResumed(_ downloadModel: MZDownloadModel, index: Int) {
275     }
276     
277     func downloadRequestCanceled(_ downloadModel: MZDownloadModel, index: Int) {
278         if downloadModel.isAudiobook(){
279             for downloadAudiobook in downloadingAudiobooks {
280                 if downloadAudiobook.waitingToDownloadUrls.index(of: downloadModel.fileURL) != nil {
281                     clearDownloadingAudiobookFromQueue(bookSlug: downloadAudiobook.bookSlug)
282
283                     delegate?.downloadManagerDownloadFailed(model: downloadModel, bookSlug:downloadAudiobook.bookSlug )
284                     return
285                 }
286             }
287         }
288
289         delegate?.downloadManagerDownloadFailed(model: downloadModel, bookSlug: downloadModel.fileName)
290     }
291     
292     func downloadRequestFinished(_ downloadModel: MZDownloadModel, index: Int) {
293         
294         // audiobook
295         if downloadModel.isAudiobook() {
296             for downloadAudiobook in downloadingAudiobooks {
297                 if let index = downloadAudiobook.waitingToDownloadUrls.index(of: downloadModel.fileURL) {
298                     downloadAudiobook.waitingToDownloadUrls.remove(at: index)
299                     downloadAudiobook.downloadedUrls.append(downloadModel.fileURL)
300                     let slug = downloadAudiobook.bookSlug
301                     // check if there is any waiting to download url, and start downloading
302                     if let firstUrl = downloadAudiobook.waitingToDownloadUrls.first(where: {$0.count > 0}) {
303                         addDownloadAudiobookTask(bookSlug: downloadAudiobook.bookSlug, fileUrl: firstUrl)
304                     }
305                     else { // otherwise, downloading is finished, notify delegates
306                         clearDownloadingAudiobookFromQueue(bookSlug: slug)
307                         delegate?.downloadManagerDownloadFinished(model: downloadModel, bookSlug: slug)
308                     }
309                 }
310             }
311         }
312         else {//ebook
313             delegate?.downloadManagerDownloadFinished(model: downloadModel, bookSlug: downloadModel.fileName)
314         }
315     }
316     
317     func downloadRequestDidFailedWithError(_ error: NSError, downloadModel: MZDownloadModel, index: Int) {
318         var bookSlug = downloadModel.fileName
319         if downloadModel.isAudiobook() {
320             for downloadAudiobook in downloadingAudiobooks {
321                 if downloadAudiobook.waitingToDownloadUrls.index(of: downloadModel.fileURL) != nil {
322                     bookSlug = downloadAudiobook.bookSlug
323                     clearDownloadingAudiobookFromQueue(bookSlug: bookSlug ?? "")
324                     continue
325                 }
326             }
327         }
328         delegate?.downloadManagerDownloadFailed(model: downloadModel, bookSlug: downloadModel.fileName)
329     }
330     
331     //Oppotunity to handle destination does not exists error
332     //This delegate will be called on the session queue so handle it appropriately
333     func downloadRequestDestinationDoestNotExists(_ downloadModel: MZDownloadModel, index: Int, location: URL) {
334         let myDownloadPath = downloadModel.destinationPath
335         if !FileManager.default.fileExists(atPath: myDownloadPath) {
336             try! FileManager.default.createDirectory(atPath: myDownloadPath, withIntermediateDirectories: true, attributes: nil)
337         }        
338         let filePath = myDownloadPath + "/" + downloadModel.fileName
339         if FileManager.default.fileExists(atPath: filePath){
340            try! FileManager.default.removeItem(atPath: filePath)
341         }
342         try! FileManager.default.moveItem(at: location, to: URL(fileURLWithPath: filePath))
343     }
344 }