NCMediaDataSource.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. //
  2. // NCMediaDataSource.swift
  3. // Nextcloud
  4. //
  5. // Created by Marino Faggiana on 25/01/24.
  6. // Copyright © 2024 Marino Faggiana. All rights reserved.
  7. //
  8. // Author Marino Faggiana <marino.faggiana@nextcloud.com>
  9. //
  10. // This program is free software: you can redistribute it and/or modify
  11. // it under the terms of the GNU General Public License as published by
  12. // the Free Software Foundation, either version 3 of the License, or
  13. // (at your option) any later version.
  14. //
  15. // This program is distributed in the hope that it will be useful,
  16. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. // GNU General Public License for more details.
  19. //
  20. // You should have received a copy of the GNU General Public License
  21. // along with this program. If not, see <http://www.gnu.org/licenses/>.
  22. //
  23. import UIKit
  24. import NextcloudKit
  25. import RealmSwift
  26. extension NCMedia {
  27. func loadDataSource() {
  28. let session = self.session
  29. DispatchQueue.global().async {
  30. if let metadatas = self.database.getResultsMetadatas(predicate: self.imageCache.getMediaPredicate(filterLivePhotoFile: true, session: session, showOnlyImages: self.showOnlyImages, showOnlyVideos: self.showOnlyVideos), sortedByKeyPath: "date") {
  31. self.dataSource = NCMediaDataSource(metadatas: metadatas)
  32. }
  33. self.collectionViewReloadData()
  34. }
  35. }
  36. func collectionViewReloadData() {
  37. DispatchQueue.main.async {
  38. self.collectionView.reloadData()
  39. self.refreshControl.endRefreshing()
  40. self.setTitleDate()
  41. }
  42. }
  43. // MARK: - Search media
  44. @objc func searchMediaUI(_ distant: Bool = false) {
  45. let session = self.session
  46. self.lockQueue.sync {
  47. guard self.isViewActived,
  48. !self.hasRunSearchMedia,
  49. !self.isPinchGestureActive,
  50. !self.showOnlyImages,
  51. !self.showOnlyVideos,
  52. !isEditMode,
  53. NCNetworking.shared.downloadThumbnailQueue.operationCount == 0,
  54. let tableAccount = database.getTableAccount(predicate: NSPredicate(format: "account == %@", session.account))
  55. else { return }
  56. self.hasRunSearchMedia = true
  57. let limit = max(collectionView.visibleCells.count * 3, 300)
  58. var lessDate = Date.distantFuture
  59. var greaterDate = Date.distantPast
  60. let countMetadatas = self.dataSource.metadatas.count
  61. let options = NKRequestOptions(timeout: 120, taskDescription: global.taskDescriptionRetrievesProperties, queue: NextcloudKit.shared.nkCommonInstance.backgroundQueue)
  62. var firstCellDate: Date?
  63. var lastCellDate: Date?
  64. if countMetadatas == 0 {
  65. self.collectionViewReloadData()
  66. }
  67. if let visibleCells = self.collectionView?.indexPathsForVisibleItems.sorted(by: { $0.row < $1.row }).compactMap({ self.collectionView?.cellForItem(at: $0) }), !distant {
  68. firstCellDate = (visibleCells.first as? NCMediaCell)?.date
  69. if firstCellDate == self.dataSource.metadatas.first?.date {
  70. lessDate = Date.distantFuture
  71. } else {
  72. if let date = firstCellDate {
  73. lessDate = Calendar.current.date(byAdding: .second, value: 1, to: date)!
  74. } else {
  75. lessDate = Date.distantFuture
  76. }
  77. }
  78. lastCellDate = (visibleCells.last as? NCMediaCell)?.date
  79. if lastCellDate == self.dataSource.metadatas.last?.date {
  80. greaterDate = Date.distantPast
  81. } else {
  82. if let date = lastCellDate {
  83. greaterDate = Calendar.current.date(byAdding: .second, value: -1, to: date)!
  84. } else {
  85. greaterDate = Date.distantPast
  86. }
  87. }
  88. }
  89. NextcloudKit.shared.nkCommonInstance.writeLog("[DEBUG] Start searchMedia with lessDate \(lessDate), greaterDate \(greaterDate), limit \(limit)")
  90. activityIndicator.startAnimating()
  91. NextcloudKit.shared.searchMedia(path: tableAccount.mediaPath,
  92. lessDate: lessDate,
  93. greaterDate: greaterDate,
  94. elementDate: "d:getlastmodified/",
  95. limit: limit,
  96. showHiddenFiles: NCKeychain().showHiddenFiles,
  97. account: self.session.account,
  98. options: options) { account, files, _, error in
  99. if error == .success, let files, session.account == account, !self.showOnlyImages, !self.showOnlyVideos {
  100. /// Removes all files in `files` that have an `ocId` present in `fileDeleted`
  101. var files = files
  102. files.removeAll { file in
  103. self.fileDeleted.contains(file.ocId)
  104. }
  105. self.fileDeleted.removeAll()
  106. /// No files, remove all
  107. if lessDate == Date.distantFuture, greaterDate == Date.distantPast, files.isEmpty {
  108. self.dataSource.metadatas.removeAll()
  109. self.collectionViewReloadData()
  110. }
  111. DispatchQueue.global().async {
  112. self.database.convertFilesToMetadatas(files, useFirstAsMetadataFolder: false) { _, metadatas in
  113. let metadatas = metadatas.filter { metadata in
  114. if let tableMetadata = self.database.getMetadataFromOcId(metadata.ocId) {
  115. return tableMetadata.status == self.global.metadataStatusNormal
  116. } else {
  117. return true
  118. }
  119. }
  120. self.database.addMetadatas(metadatas)
  121. if self.dataSource.addMetadatas(metadatas) {
  122. self.collectionViewReloadData()
  123. }
  124. DispatchQueue.main.async {
  125. if let firstCellDate, let lastCellDate, self.isViewActived {
  126. DispatchQueue.global().async {
  127. let predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ NSPredicate(format: "date >= %@ AND date =< %@", lastCellDate as NSDate, firstCellDate as NSDate), self.imageCache.getMediaPredicate(filterLivePhotoFile: false, session: session, showOnlyImages: self.showOnlyImages, showOnlyVideos: self.showOnlyVideos)])
  128. if let resultsMetadatas = NCManageDatabase.shared.getResultsMetadatas(predicate: predicate) {
  129. for metadata in resultsMetadatas where !self.filesExists.contains(metadata.ocId) {
  130. if NCNetworking.shared.fileExistsQueue.operations.filter({ ($0 as? NCOperationFileExists)?.ocId == metadata.ocId }).isEmpty {
  131. NCNetworking.shared.fileExistsQueue.addOperation(NCOperationFileExists(metadata: metadata))
  132. }
  133. }
  134. }
  135. }
  136. }
  137. }
  138. }
  139. }
  140. } else {
  141. NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Media search new media error code \(error.errorCode) " + error.errorDescription)
  142. self.collectionViewReloadData()
  143. }
  144. DispatchQueue.main.async {
  145. self.activityIndicator.stopAnimating()
  146. }
  147. self.hasRunSearchMedia = false
  148. }
  149. }
  150. }
  151. }
  152. // MARK: -
  153. public class NCMediaDataSource: NSObject {
  154. public class Metadata: NSObject {
  155. let date: Date
  156. let etag: String
  157. let imageSize: CGSize
  158. let isImage: Bool
  159. let isLivePhoto: Bool
  160. let isVideo: Bool
  161. let ocId: String
  162. init(date: Date,
  163. etag: String,
  164. imageSize: CGSize,
  165. isImage: Bool,
  166. isLivePhoto: Bool,
  167. isVideo: Bool,
  168. ocId: String) {
  169. self.date = date
  170. self.etag = etag
  171. self.imageSize = imageSize
  172. self.isImage = isImage
  173. self.isLivePhoto = isLivePhoto
  174. self.isVideo = isVideo
  175. self.ocId = ocId
  176. }
  177. }
  178. private let utilityFileSystem = NCUtilityFileSystem()
  179. private let global = NCGlobal.shared
  180. var metadatas: [Metadata] = []
  181. override init() { super.init() }
  182. init(metadatas: Results<tableMetadata>) {
  183. super.init()
  184. self.metadatas.removeAll()
  185. metadatas.forEach { metadata in
  186. let metadata = getMetadataFromTableMetadata(metadata)
  187. self.metadatas.append(metadata)
  188. }
  189. }
  190. private func insertInMetadatas(metadata: Metadata) {
  191. for i in 0..<self.metadatas.count {
  192. if (metadata.date as Date) > self.metadatas[i].date {
  193. self.metadatas.insert(metadata, at: i)
  194. return
  195. }
  196. }
  197. self.metadatas.append(metadata)
  198. }
  199. private func getMetadataFromTableMetadata(_ metadata: tableMetadata) -> Metadata {
  200. return Metadata(date: metadata.date as Date,
  201. etag: metadata.etag,
  202. imageSize: CGSize(width: metadata.width, height: metadata.height),
  203. isImage: metadata.classFile == NKCommon.TypeClassFile.image.rawValue,
  204. isLivePhoto: !metadata.livePhotoFile.isEmpty,
  205. isVideo: metadata.classFile == NKCommon.TypeClassFile.video.rawValue,
  206. ocId: metadata.ocId)
  207. }
  208. // MARK: -
  209. func getMetadata(indexPath: IndexPath) -> Metadata? {
  210. if indexPath.row < self.metadatas.count {
  211. return self.metadatas[indexPath.row]
  212. }
  213. return nil
  214. }
  215. func getMetadatas(indexPaths: [IndexPath]) -> [Metadata] {
  216. var metadatas: [Metadata] = []
  217. for indexPath in indexPaths {
  218. if indexPath.row < self.metadatas.count {
  219. metadatas.append(self.metadatas[indexPath.row])
  220. }
  221. }
  222. return metadatas
  223. }
  224. func removeMetadata(_ ocId: [String]) {
  225. self.metadatas.removeAll { item in
  226. ocId.contains(item.ocId)
  227. }
  228. }
  229. func addMetadatas(_ metadatas: [tableMetadata]) -> Bool {
  230. var metadatasToInsert: [Metadata] = []
  231. for tableMetadata in metadatas {
  232. let metadata = getMetadataFromTableMetadata(tableMetadata)
  233. if metadata.isLivePhoto, metadata.isVideo { continue }
  234. if let index = self.metadatas.firstIndex(where: { $0.ocId == tableMetadata.ocId }) {
  235. self.metadatas[index] = metadata
  236. } else {
  237. metadatasToInsert.append(metadata)
  238. }
  239. }
  240. // • For many new elements (e.g., hundreds or thousands): It might be more efficient to add all the elements and then sort, especially if the sorting cost O(n \log n) is manageable and the final sort is preferable to handling many individual insertions.
  241. // • For a few new elements (fewer than 100): Inserting each element into the correct position might be simpler and less costly, particularly if the array isn’t too large.
  242. if !metadatasToInsert.isEmpty {
  243. if metadatasToInsert.count < 100 {
  244. for metadata in metadatasToInsert {
  245. self.insertInMetadatas(metadata: metadata)
  246. }
  247. } else {
  248. for metadata in metadatasToInsert {
  249. self.metadatas.append(metadata)
  250. }
  251. self.metadatas = self.metadatas.sorted { $0.date > $1.date }
  252. }
  253. return true
  254. }
  255. return false
  256. }
  257. }