NCMediaDataSource.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  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. /// No files, remove all
  101. if lessDate == Date.distantFuture, greaterDate == Date.distantPast, files.isEmpty {
  102. self.dataSource.metadatas.removeAll()
  103. self.collectionViewReloadData()
  104. }
  105. DispatchQueue.global().async {
  106. self.database.convertFilesToMetadatas(files, useFirstAsMetadataFolder: false) { _, metadatas in
  107. let metadatas = metadatas.filter { metadata in
  108. if let tableMetadata = self.database.getMetadataFromOcId(metadata.ocId) {
  109. return tableMetadata.status == self.global.metadataStatusNormal
  110. } else {
  111. return true
  112. }
  113. }
  114. self.database.addMetadatas(metadatas)
  115. if self.dataSource.addMetadatas(metadatas) {
  116. self.collectionViewReloadData()
  117. }
  118. DispatchQueue.main.async {
  119. if let firstCellDate, let lastCellDate, self.isViewActived {
  120. DispatchQueue.global().async {
  121. 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)])
  122. if let resultsMetadatas = NCManageDatabase.shared.getResultsMetadatas(predicate: predicate) {
  123. for metadata in resultsMetadatas where !self.filesExists.contains(metadata.ocId) {
  124. if NCNetworking.shared.fileExistsQueue.operations.filter({ ($0 as? NCOperationFileExists)?.ocId == metadata.ocId }).isEmpty {
  125. NCNetworking.shared.fileExistsQueue.addOperation(NCOperationFileExists(metadata: metadata))
  126. }
  127. }
  128. }
  129. }
  130. }
  131. }
  132. }
  133. }
  134. } else {
  135. NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Media search new media error code \(error.errorCode) " + error.errorDescription)
  136. self.collectionViewReloadData()
  137. }
  138. DispatchQueue.main.async {
  139. self.activityIndicator.stopAnimating()
  140. }
  141. self.hasRunSearchMedia = false
  142. }
  143. }
  144. }
  145. }
  146. // MARK: -
  147. public class NCMediaDataSource: NSObject {
  148. public class Metadata: NSObject {
  149. let date: Date
  150. let etag: String
  151. let imageSize: CGSize
  152. let isImage: Bool
  153. let isLivePhoto: Bool
  154. let isVideo: Bool
  155. let ocId: String
  156. init(date: Date,
  157. etag: String,
  158. imageSize: CGSize,
  159. isImage: Bool,
  160. isLivePhoto: Bool,
  161. isVideo: Bool,
  162. ocId: String) {
  163. self.date = date
  164. self.etag = etag
  165. self.imageSize = imageSize
  166. self.isImage = isImage
  167. self.isLivePhoto = isLivePhoto
  168. self.isVideo = isVideo
  169. self.ocId = ocId
  170. }
  171. }
  172. private let utilityFileSystem = NCUtilityFileSystem()
  173. private let global = NCGlobal.shared
  174. var metadatas: [Metadata] = []
  175. override init() { super.init() }
  176. init(metadatas: Results<tableMetadata>) {
  177. super.init()
  178. self.metadatas.removeAll()
  179. metadatas.forEach { metadata in
  180. let metadata = getMetadataFromTableMetadata(metadata)
  181. self.metadatas.append(metadata)
  182. }
  183. }
  184. private func insertInMetadatas(metadata: Metadata) {
  185. for i in 0..<self.metadatas.count {
  186. if (metadata.date as Date) > self.metadatas[i].date {
  187. self.metadatas.insert(metadata, at: i)
  188. return
  189. }
  190. }
  191. self.metadatas.append(metadata)
  192. }
  193. private func getMetadataFromTableMetadata(_ metadata: tableMetadata) -> Metadata {
  194. return Metadata(date: metadata.date as Date,
  195. etag: metadata.etag,
  196. imageSize: CGSize(width: metadata.width, height: metadata.height),
  197. isImage: metadata.classFile == NKCommon.TypeClassFile.image.rawValue,
  198. isLivePhoto: !metadata.livePhotoFile.isEmpty,
  199. isVideo: metadata.classFile == NKCommon.TypeClassFile.video.rawValue,
  200. ocId: metadata.ocId)
  201. }
  202. // MARK: -
  203. func getMetadata(indexPath: IndexPath) -> Metadata? {
  204. if indexPath.row < self.metadatas.count {
  205. return self.metadatas[indexPath.row]
  206. }
  207. return nil
  208. }
  209. func getMetadatas(indexPaths: [IndexPath]) -> [Metadata] {
  210. var metadatas: [Metadata] = []
  211. for indexPath in indexPaths {
  212. if indexPath.row < self.metadatas.count {
  213. metadatas.append(self.metadatas[indexPath.row])
  214. }
  215. }
  216. return metadatas
  217. }
  218. func removeMetadata(_ ocId: [String]) {
  219. self.metadatas.removeAll { item in
  220. ocId.contains(item.ocId)
  221. }
  222. }
  223. func addMetadatas(_ metadatas: [tableMetadata]) -> Bool {
  224. var metadatasToInsert: [Metadata] = []
  225. for tableMetadata in metadatas {
  226. let metadata = getMetadataFromTableMetadata(tableMetadata)
  227. if metadata.isLivePhoto, metadata.isVideo { continue }
  228. if let index = self.metadatas.firstIndex(where: { $0.ocId == tableMetadata.ocId }) {
  229. self.metadatas[index] = metadata
  230. } else {
  231. metadatasToInsert.append(metadata)
  232. }
  233. }
  234. // • 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.
  235. // • 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.
  236. if !metadatasToInsert.isEmpty {
  237. if metadatasToInsert.count < 100 {
  238. for metadata in metadatasToInsert {
  239. self.insertInMetadatas(metadata: metadata)
  240. }
  241. } else {
  242. for metadata in metadatasToInsert {
  243. self.metadatas.append(metadata)
  244. }
  245. self.metadatas = self.metadatas.sorted { $0.date > $1.date }
  246. }
  247. return true
  248. }
  249. return false
  250. }
  251. }