MediaLibraryService.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  1. /*****************************************************************************
  2. * MediaLibraryService.swift
  3. * VLC for iOS
  4. *****************************************************************************
  5. * Copyright © 2018 VideoLAN. All rights reserved.
  6. * Copyright © 2018 Videolabs
  7. *
  8. * Authors: Soomin Lee <bubu # mikan.io>
  9. *
  10. * Refer to the COPYING file of the official project for license.
  11. *****************************************************************************/
  12. // MARK: - Notification names
  13. extension Notification.Name {
  14. static let VLCNewFileAddedNotification = Notification.Name("NewFileAddedNotification")
  15. }
  16. // For objc
  17. extension NSNotification {
  18. @objc static let VLCNewFileAddedNotification = Notification.Name.VLCNewFileAddedNotification
  19. }
  20. // MARK: -
  21. @objc protocol MediaLibraryObserver: class {
  22. // Video
  23. @objc optional func medialibrary(_ medialibrary: MediaLibraryService,
  24. didModifyVideo video: [VLCMLMedia])
  25. @objc optional func medialibrary(_ medialibrary: MediaLibraryService,
  26. didDeleteMediaWithIds ids: [NSNumber])
  27. @objc optional func medialibrary(_ medialibrary: MediaLibraryService,
  28. didAddVideos videos: [VLCMLMedia])
  29. @objc optional func medialibrary(_ medialibrary: MediaLibraryService,
  30. didAddShowEpisodes showEpisodes: [VLCMLMedia])
  31. @objc optional func medialibrary(_ medialibrary: MediaLibraryService,
  32. thumbnailReady media: VLCMLMedia)
  33. // Audio
  34. @objc optional func medialibrary(_ medialibrary: MediaLibraryService,
  35. didAddAudios audios: [VLCMLMedia])
  36. @objc optional func medialibrary(_ medialibrary: MediaLibraryService,
  37. didAddArtists artists: [VLCMLArtist])
  38. @objc optional func medialibrary(_ medialibrary: MediaLibraryService,
  39. didDeleteArtistsWithIds artistsIds: [NSNumber])
  40. @objc optional func medialibrary(_ medialibrary: MediaLibraryService,
  41. didAddAlbums albums: [VLCMLAlbum])
  42. @objc optional func medialibrary(_ medialibrary: MediaLibraryService,
  43. didDeleteAlbumsWithIds albumsIds: [NSNumber])
  44. @objc optional func medialibrary(_ medialibrary: MediaLibraryService,
  45. didAddAlbumTracks albumTracks: [VLCMLMedia])
  46. @objc optional func medialibrary(_ medialibrary: MediaLibraryService,
  47. didAddGenres genres: [VLCMLGenre])
  48. // Playlist
  49. @objc optional func medialibrary(_ medialibrary: MediaLibraryService,
  50. didAddPlaylists playlists: [VLCMLPlaylist])
  51. @objc optional func medialibrary(_ medialibrary: MediaLibraryService,
  52. didDeletePlaylistsWithIds playlistsIds: [NSNumber])
  53. }
  54. // MARK: -
  55. protocol MediaLibraryMigrationDelegate: class {
  56. func medialibraryDidStartMigration(_ medialibrary: MediaLibraryService)
  57. func medialibraryDidFinishMigration(_ medialibrary: MediaLibraryService)
  58. func medialibraryDidStopMigration(_ medialibrary: MediaLibraryService)
  59. }
  60. // MARK: -
  61. class MediaLibraryService: NSObject {
  62. private static let databaseName: String = "medialibrary.db"
  63. private static let migrationKey: String = "MigratedToVLCMediaLibraryKit"
  64. private var didMigrate = UserDefaults.standard.bool(forKey: MediaLibraryService.migrationKey)
  65. private var didFinishDiscovery = false
  66. // Using ObjectIdentifier to avoid duplication and facilitate
  67. // identification of observing object
  68. private var observers = [ObjectIdentifier: Observer]()
  69. private var medialib: VLCMediaLibrary!
  70. weak var migrationDelegate: MediaLibraryMigrationDelegate?
  71. override init() {
  72. super.init()
  73. medialib = VLCMediaLibrary()
  74. medialib.delegate = self
  75. setupMediaLibrary()
  76. NotificationCenter.default.addObserver(self, selector: #selector(reload),
  77. name: .VLCNewFileAddedNotification, object: nil)
  78. }
  79. }
  80. // MARK: - Private initializers
  81. private extension MediaLibraryService {
  82. private func setupMediaDiscovery(at path: String) {
  83. let mediaFileDiscoverer = VLCMediaFileDiscoverer.sharedInstance()
  84. mediaFileDiscoverer?.directoryPath = path
  85. mediaFileDiscoverer?.addObserver(self)
  86. mediaFileDiscoverer?.startDiscovering()
  87. }
  88. private func setupMediaLibrary() {
  89. guard let documentPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first,
  90. let libraryPath = NSSearchPathForDirectoriesInDomains(.libraryDirectory, .userDomainMask, true).first else {
  91. preconditionFailure("MediaLibraryService: Unable to init medialibrary.")
  92. }
  93. setupMediaDiscovery(at: documentPath)
  94. let databasePath = libraryPath + "/MediaLibrary/" + MediaLibraryService.databaseName
  95. let thumbnailPath = libraryPath + "/MediaLibrary/Thumbnails"
  96. do {
  97. try FileManager.default.createDirectory(atPath: thumbnailPath,
  98. withIntermediateDirectories: true)
  99. } catch let error as NSError {
  100. assertionFailure("Failed to create directory: \(error.localizedDescription)")
  101. }
  102. let medialibraryStatus = medialib.setupMediaLibrary(databasePath: databasePath,
  103. thumbnailPath: thumbnailPath)
  104. switch medialibraryStatus {
  105. case .success:
  106. guard medialib.start() else {
  107. assertionFailure("MediaLibraryService: Medialibrary failed to start.")
  108. return
  109. }
  110. medialib.reload()
  111. medialib.discover(onEntryPoint: "file://" + documentPath)
  112. case .alreadyInitialized:
  113. assertionFailure("MediaLibraryService: Medialibrary already initialized.")
  114. case .failed:
  115. preconditionFailure("MediaLibraryService: Failed to setup medialibrary.")
  116. case .dbReset:
  117. // should still start and discover but warn the user that the db has been wipped
  118. assertionFailure("MediaLibraryService: The database was resetted, please re-configure.")
  119. }
  120. }
  121. }
  122. // MARK: - Migration
  123. private extension MediaLibraryService {
  124. func startMigrationIfNeeded() {
  125. guard !didMigrate else {
  126. return
  127. }
  128. migrationDelegate?.medialibraryDidStartMigration(self)
  129. guard migrateToNewMediaLibrary() else {
  130. migrationDelegate?.medialibraryDidStopMigration(self)
  131. return
  132. }
  133. migrationDelegate?.medialibraryDidFinishMigration(self)
  134. }
  135. func migrateMedia(_ oldMedialibrary: MLMediaLibrary) -> Bool {
  136. guard let allFiles = MLFile.allFiles() as? [MLFile] else {
  137. assertionFailure("MediaLibraryService: Migration: Unable to retreive all files")
  138. return false
  139. }
  140. for media in allFiles {
  141. if let newMedia = fetchMedia(with: media.url) {
  142. newMedia.updateTitle(media.title)
  143. newMedia.setPlayCount(media.playCount.uint32Value)
  144. newMedia.setMetadataOf(.progress, intValue: media.lastPosition.int64Value)
  145. newMedia.setMetadataOf(.seen, intValue: media.unread.int64Value)
  146. // Only delete files that are not in playlist
  147. if media.labels.isEmpty {
  148. oldMedialibrary.remove(media)
  149. }
  150. }
  151. }
  152. oldMedialibrary.save()
  153. return true
  154. }
  155. // This private method migrates old playlist and removes file and playlist
  156. // from the old medialibrary.
  157. // Note: This removes **only** files that are in a playlist
  158. func migratePlaylists(_ oldMedialibrary: MLMediaLibrary) -> Bool {
  159. guard let allLabels = MLLabel.allLabels() as? [MLLabel] else {
  160. assertionFailure("MediaLibraryService: Migration: Unable to retreive all labels")
  161. return false
  162. }
  163. for label in allLabels {
  164. let newPlaylist = createPlaylist(with: label.name)
  165. guard let files = label.files as? Set<MLFile> else {
  166. assertionFailure("MediaLibraryService: Migration: Unable to retreive files from label")
  167. oldMedialibrary.remove(label)
  168. continue
  169. }
  170. for file in files {
  171. if let newMedia = fetchMedia(with: file.url) {
  172. if newPlaylist.appendMedia(withIdentifier: newMedia.identifier()) {
  173. oldMedialibrary.remove(file)
  174. }
  175. }
  176. }
  177. oldMedialibrary.remove(label)
  178. }
  179. oldMedialibrary.save()
  180. return true
  181. }
  182. func migrateToNewMediaLibrary() -> Bool {
  183. guard let oldMedialibrary = MLMediaLibrary.sharedMediaLibrary() as? MLMediaLibrary else {
  184. assertionFailure("MediaLibraryService: Migration: Unable to retreive old medialibrary")
  185. return false
  186. }
  187. if migrateMedia(oldMedialibrary) && migratePlaylists(oldMedialibrary) {
  188. UserDefaults.standard.set(true, forKey: MediaLibraryService.migrationKey)
  189. return true
  190. }
  191. return false
  192. }
  193. }
  194. // MARK: - Observer
  195. private extension MediaLibraryService {
  196. struct Observer {
  197. weak var observer: MediaLibraryObserver?
  198. }
  199. }
  200. extension MediaLibraryService {
  201. func addObserver(_ observer: MediaLibraryObserver) {
  202. let identifier = ObjectIdentifier(observer)
  203. observers[identifier] = Observer(observer: observer)
  204. }
  205. func removeObserver(_ observer: MediaLibraryObserver) {
  206. let identifier = ObjectIdentifier(observer)
  207. observers.removeValue(forKey: identifier)
  208. }
  209. }
  210. // MARK: - Helpers
  211. extension MediaLibraryService {
  212. @objc func reload() {
  213. medialib.reload()
  214. }
  215. /// Returns number of *ALL* files(audio and video) present in the medialibrary database
  216. func numberOfFiles() -> Int {
  217. var media = medialib.audioFiles(with: .filename, desc: false)
  218. media += medialib.videoFiles(with: .filename, desc: false)
  219. return media.count
  220. }
  221. /// Returns *ALL* file found for a specified VLCMLMediaType
  222. ///
  223. /// - Parameter type: Type of the media
  224. /// - Returns: Array of VLCMLMedia
  225. func media(ofType type: VLCMLMediaType, sortingCriteria sort: VLCMLSortingCriteria = .filename, desc: Bool = false) -> [VLCMLMedia] {
  226. return type == .video ? medialib.videoFiles(with: sort, desc: desc) : medialib.audioFiles(with: sort, desc: desc)
  227. }
  228. func fetchMedia(with mrl: URL?) -> VLCMLMedia? {
  229. guard let mrl = mrl else {
  230. return nil
  231. }
  232. return medialib.media(withMrl: mrl)
  233. }
  234. }
  235. // MARK: - Audio methods
  236. extension MediaLibraryService {
  237. func artists(sortingCriteria sort: VLCMLSortingCriteria = .artist, desc: Bool = false) -> [VLCMLArtist] {
  238. return medialib.artists(with: sort, desc: desc, all: true)
  239. }
  240. func albums(sortingCriteria sort: VLCMLSortingCriteria = .album, desc: Bool = false) -> [VLCMLAlbum] {
  241. return medialib.albums(with: sort, desc: desc)
  242. }
  243. }
  244. // MARK: - Video methods
  245. extension MediaLibraryService {
  246. func requestThumbnail(for media: [VLCMLMedia]) {
  247. media.forEach() {
  248. guard !$0.isThumbnailGenerated() else { return }
  249. if !medialib.requestThumbnail(for: $0) {
  250. assertionFailure("MediaLibraryService: Failed to generate thumbnail for: \($0.identifier())")
  251. }
  252. }
  253. }
  254. }
  255. // MARK: - Playlist methods
  256. extension MediaLibraryService {
  257. func createPlaylist(with name: String) -> VLCMLPlaylist {
  258. return medialib.createPlaylist(withName: name)
  259. }
  260. func deletePlaylist(with identifier: VLCMLIdentifier) -> Bool {
  261. return medialib.deletePlaylist(withIdentifier: identifier)
  262. }
  263. func playlists(sortingCriteria sort: VLCMLSortingCriteria = .default, desc: Bool = false) -> [VLCMLPlaylist] {
  264. return medialib.playlists(with: sort, desc: desc)
  265. }
  266. }
  267. // MARK: - Genre methods
  268. extension MediaLibraryService {
  269. func genres(sortingCriteria sort: VLCMLSortingCriteria = .default, desc: Bool = false) -> [VLCMLGenre] {
  270. return medialib.genres(with: sort, desc: desc)
  271. }
  272. }
  273. // MARK: - VLCMediaFileDiscovererDelegate
  274. extension MediaLibraryService: VLCMediaFileDiscovererDelegate {
  275. func mediaFileAdded(_ filePath: String!, loading isLoading: Bool) {
  276. guard !isLoading else {
  277. return
  278. }
  279. /* exclude media files from backup (QA1719) */
  280. var excludeURL = URL(fileURLWithPath: filePath)
  281. var resourceValue = URLResourceValues()
  282. resourceValue.isExcludedFromBackup = true
  283. do {
  284. try excludeURL.setResourceValues(resourceValue)
  285. } catch let error {
  286. assertionFailure("MediaLibraryService: VLCMediaFileDiscovererDelegate: \(error.localizedDescription)")
  287. }
  288. reload()
  289. }
  290. func mediaFileDeleted(_ filePath: String!) {
  291. reload()
  292. }
  293. }
  294. // MARK: - VLCMediaLibraryDelegate - Media
  295. extension MediaLibraryService: VLCMediaLibraryDelegate {
  296. func medialibrary(_ medialibrary: VLCMediaLibrary, didAddMedia media: [VLCMLMedia]) {
  297. let videos = media.filter {( $0.type() == .video )}
  298. let audio = media.filter {( $0.type() == .audio )}
  299. // thumbnails only for videos
  300. requestThumbnail(for: videos)
  301. for observer in observers {
  302. observer.value.observer?.medialibrary?(self, didAddVideos: videos)
  303. observer.value.observer?.medialibrary?(self, didAddAudios: audio)
  304. }
  305. }
  306. func medialibrary(_ medialibrary: VLCMediaLibrary, didModifyMedia media: [VLCMLMedia]) {
  307. let showEpisodes = media.filter {( $0.subtype() == .showEpisode )}
  308. let albumTrack = media.filter {( $0.subtype() == .albumTrack )}
  309. for observer in observers {
  310. observer.value.observer?.medialibrary?(self, didAddShowEpisodes: showEpisodes)
  311. observer.value.observer?.medialibrary?(self, didAddAlbumTracks: albumTrack)
  312. }
  313. }
  314. func medialibrary(_ medialibrary: VLCMediaLibrary, didDeleteMediaWithIds mediaIds: [NSNumber]) {
  315. for observer in observers {
  316. observer.value.observer?.medialibrary?(self, didDeleteMediaWithIds: mediaIds)
  317. }
  318. }
  319. func medialibrary(_ medialibrary: VLCMediaLibrary, thumbnailReadyFor media: VLCMLMedia, withSuccess success: Bool) {
  320. for observer in observers {
  321. observer.value.observer?.medialibrary?(self, thumbnailReady: media)
  322. }
  323. }
  324. }
  325. // MARK: - VLCMediaLibraryDelegate - Artists
  326. extension MediaLibraryService {
  327. func medialibrary(_ medialibrary: VLCMediaLibrary, didAdd artists: [VLCMLArtist]) {
  328. for observer in observers {
  329. observer.value.observer?.medialibrary?(self, didAddArtists: artists)
  330. }
  331. }
  332. func medialibrary(_ medialibrary: VLCMediaLibrary, didDeleteArtistsWithIds artistsIds: [NSNumber]) {
  333. for observer in observers {
  334. observer.value.observer?.medialibrary?(self, didDeleteArtistsWithIds: artistsIds)
  335. }
  336. }
  337. }
  338. // MARK: - VLCMediaLibraryDelegate - Albums
  339. extension MediaLibraryService {
  340. func medialibrary(_ medialibrary: VLCMediaLibrary, didAdd albums: [VLCMLAlbum]) {
  341. for observer in observers {
  342. observer.value.observer?.medialibrary?(self, didAddAlbums: albums)
  343. }
  344. }
  345. func medialibrary(_ medialibrary: VLCMediaLibrary, didDeleteAlbumsWithIds albumsIds: [NSNumber]) {
  346. for observer in observers {
  347. observer.value.observer?.medialibrary?(self, didDeleteAlbumsWithIds: albumsIds)
  348. }
  349. }
  350. }
  351. // MARK: - VLCMediaLibraryDelegate - Playlists
  352. extension MediaLibraryService {
  353. func medialibrary(_ medialibrary: VLCMediaLibrary, didAdd playlists: [VLCMLPlaylist]) {
  354. for observer in observers {
  355. observer.value.observer?.medialibrary?(self, didAddPlaylists: playlists)
  356. }
  357. }
  358. func medialibrary(_ medialibrary: VLCMediaLibrary, didDeletePlaylistsWithIds playlistsIds: [NSNumber]) {
  359. for observer in observers {
  360. observer.value.observer?.medialibrary?(self, didDeletePlaylistsWithIds: playlistsIds)
  361. }
  362. }
  363. }
  364. // MARK: - VLCMediaLibraryDelegate - Discovery
  365. extension MediaLibraryService {
  366. func medialibrary(_ medialibrary: VLCMediaLibrary, didStartDiscovery entryPoint: String) {
  367. }
  368. func medialibrary(_ medialibrary: VLCMediaLibrary, didCompleteDiscovery entryPoint: String) {
  369. didFinishDiscovery = true
  370. }
  371. func medialibrary(_ medialibrary: VLCMediaLibrary, didProgressDiscovery entryPoint: String) {
  372. }
  373. func medialibrary(_ medialibrary: VLCMediaLibrary, didUpdateParsingStatsWithPercent percent: UInt32) {
  374. if didFinishDiscovery && percent == 100 {
  375. startMigrationIfNeeded()
  376. }
  377. }
  378. }