MediaLibraryService.swift 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  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. didAddTracks tracks: [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. @objc optional func medialibrary(_ medialibrary: MediaLibraryService,
  49. didDeleteGenresWithIds genreIds: [NSNumber])
  50. // Playlist
  51. @objc optional func medialibrary(_ medialibrary: MediaLibraryService,
  52. didAddPlaylists playlists: [VLCMLPlaylist])
  53. @objc optional func medialibrary(_ medialibrary: MediaLibraryService,
  54. didModifyPlaylists playlists: [VLCMLPlaylist])
  55. @objc optional func medialibrary(_ medialibrary: MediaLibraryService,
  56. didDeletePlaylistsWithIds playlistsIds: [NSNumber])
  57. }
  58. // MARK: -
  59. protocol MediaLibraryMigrationDelegate: class {
  60. func medialibraryDidStartMigration(_ medialibrary: MediaLibraryService)
  61. func medialibraryDidFinishMigration(_ medialibrary: MediaLibraryService)
  62. func medialibraryDidStopMigration(_ medialibrary: MediaLibraryService)
  63. }
  64. // MARK: -
  65. class MediaLibraryService: NSObject {
  66. private static let databaseName: String = "medialibrary.db"
  67. private static let migrationKey: String = "MigratedToVLCMediaLibraryKit"
  68. private var didMigrate = UserDefaults.standard.bool(forKey: MediaLibraryService.migrationKey)
  69. private var didFinishDiscovery = false
  70. // Using ObjectIdentifier to avoid duplication and facilitate
  71. // identification of observing object
  72. private var observers = [ObjectIdentifier: Observer]()
  73. private var medialib: VLCMediaLibrary!
  74. weak var migrationDelegate: MediaLibraryMigrationDelegate?
  75. override init() {
  76. super.init()
  77. medialib = VLCMediaLibrary()
  78. medialib.delegate = self
  79. setupMediaLibrary()
  80. NotificationCenter.default.addObserver(self, selector: #selector(reload),
  81. name: .VLCNewFileAddedNotification, object: nil)
  82. }
  83. }
  84. // MARK: - Private initializers
  85. private extension MediaLibraryService {
  86. private func setupMediaDiscovery(at path: String) {
  87. let mediaFileDiscoverer = VLCMediaFileDiscoverer.sharedInstance()
  88. mediaFileDiscoverer?.directoryPath = path
  89. mediaFileDiscoverer?.addObserver(self)
  90. mediaFileDiscoverer?.startDiscovering()
  91. }
  92. private func setupMediaLibrary() {
  93. guard let documentPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first,
  94. let libraryPath = NSSearchPathForDirectoriesInDomains(.libraryDirectory, .userDomainMask, true).first else {
  95. preconditionFailure("MediaLibraryService: Unable to init medialibrary.")
  96. }
  97. setupMediaDiscovery(at: documentPath)
  98. let databasePath = libraryPath + "/MediaLibrary/" + MediaLibraryService.databaseName
  99. let thumbnailPath = libraryPath + "/MediaLibrary/Thumbnails"
  100. do {
  101. try FileManager.default.createDirectory(atPath: thumbnailPath,
  102. withIntermediateDirectories: true)
  103. } catch let error as NSError {
  104. assertionFailure("Failed to create directory: \(error.localizedDescription)")
  105. }
  106. let medialibraryStatus = medialib.setupMediaLibrary(databasePath: databasePath,
  107. thumbnailPath: thumbnailPath)
  108. switch medialibraryStatus {
  109. case .success:
  110. guard medialib.start() else {
  111. assertionFailure("MediaLibraryService: Medialibrary failed to start.")
  112. return
  113. }
  114. medialib.reload()
  115. medialib.discover(onEntryPoint: "file://" + documentPath)
  116. case .alreadyInitialized:
  117. assertionFailure("MediaLibraryService: Medialibrary already initialized.")
  118. case .failed:
  119. preconditionFailure("MediaLibraryService: Failed to setup medialibrary.")
  120. case .dbReset:
  121. // should still start and discover but warn the user that the db has been wipped
  122. assertionFailure("MediaLibraryService: The database was resetted, please re-configure.")
  123. @unknown default:
  124. assertionFailure("MediaLibraryService: unhandled case")
  125. }
  126. }
  127. }
  128. // MARK: - Migration
  129. private extension MediaLibraryService {
  130. func startMigrationIfNeeded() {
  131. guard !didMigrate else {
  132. return
  133. }
  134. migrationDelegate?.medialibraryDidStartMigration(self)
  135. guard migrateToNewMediaLibrary() else {
  136. migrationDelegate?.medialibraryDidStopMigration(self)
  137. return
  138. }
  139. migrationDelegate?.medialibraryDidFinishMigration(self)
  140. }
  141. func migrateMedia(_ oldMedialibrary: MLMediaLibrary) -> Bool {
  142. guard let allFiles = MLFile.allFiles() as? [MLFile] else {
  143. assertionFailure("MediaLibraryService: Migration: Unable to retrieve all files")
  144. return false
  145. }
  146. for media in allFiles {
  147. if let newMedia = fetchMedia(with: media.url) {
  148. newMedia.updateTitle(media.title)
  149. newMedia.setPlayCount(media.playCount.uint32Value)
  150. newMedia.setMetadataOf(.progress, intValue: media.lastPosition.int64Value)
  151. newMedia.setMetadataOf(.seen, intValue: media.unread.int64Value)
  152. // Only delete files that are not in playlist
  153. if media.labels.isEmpty {
  154. oldMedialibrary.remove(media)
  155. }
  156. }
  157. }
  158. oldMedialibrary.save()
  159. return true
  160. }
  161. // This private method migrates old playlist and removes file and playlist
  162. // from the old medialibrary.
  163. // Note: This removes **only** files that are in a playlist
  164. func migratePlaylists(_ oldMedialibrary: MLMediaLibrary) -> Bool {
  165. guard let allLabels = MLLabel.allLabels() as? [MLLabel] else {
  166. assertionFailure("MediaLibraryService: Migration: Unable to retrieve all labels")
  167. return false
  168. }
  169. for label in allLabels {
  170. guard let newPlaylist = createPlaylist(with: label.name) else {
  171. assertionFailure("MediaLibraryService: Migration: Unable to create playlist.")
  172. continue
  173. }
  174. guard let files = label.files as? Set<MLFile> else {
  175. assertionFailure("MediaLibraryService: Migration: Unable to retrieve files from label")
  176. oldMedialibrary.remove(label)
  177. continue
  178. }
  179. for file in files {
  180. if let newMedia = fetchMedia(with: file.url) {
  181. if newPlaylist.appendMedia(withIdentifier: newMedia.identifier()) {
  182. oldMedialibrary.remove(file)
  183. }
  184. }
  185. }
  186. oldMedialibrary.remove(label)
  187. }
  188. oldMedialibrary.save()
  189. return true
  190. }
  191. func migrateToNewMediaLibrary() -> Bool {
  192. guard let oldMedialibrary = MLMediaLibrary.sharedMediaLibrary() as? MLMediaLibrary else {
  193. assertionFailure("MediaLibraryService: Migration: Unable to retrieve old medialibrary")
  194. return false
  195. }
  196. if migrateMedia(oldMedialibrary) && migratePlaylists(oldMedialibrary) {
  197. UserDefaults.standard.set(true, forKey: MediaLibraryService.migrationKey)
  198. didMigrate = true
  199. return true
  200. }
  201. return false
  202. }
  203. }
  204. // MARK: - Observer
  205. private extension MediaLibraryService {
  206. struct Observer {
  207. weak var observer: MediaLibraryObserver?
  208. }
  209. }
  210. extension MediaLibraryService {
  211. func addObserver(_ observer: MediaLibraryObserver) {
  212. let identifier = ObjectIdentifier(observer)
  213. observers[identifier] = Observer(observer: observer)
  214. }
  215. func removeObserver(_ observer: MediaLibraryObserver) {
  216. let identifier = ObjectIdentifier(observer)
  217. observers.removeValue(forKey: identifier)
  218. }
  219. }
  220. // MARK: - Helpers
  221. @objc extension MediaLibraryService {
  222. @objc func reload() {
  223. medialib.reload()
  224. }
  225. /// Returns number of *ALL* files(audio and video) present in the medialibrary database
  226. func numberOfFiles() -> Int {
  227. return (medialib.audioFiles()?.count ?? 0) + (medialib.videoFiles()?.count ?? 0)
  228. }
  229. /// Returns *ALL* file found for a specified VLCMLMediaType
  230. ///
  231. /// - Parameter type: Type of the media
  232. /// - Returns: Array of VLCMLMedia
  233. func media(ofType type: VLCMLMediaType,
  234. sortingCriteria sort: VLCMLSortingCriteria = .alpha,
  235. desc: Bool = false) -> [VLCMLMedia] {
  236. return type == .video ? medialib.videoFiles(with: sort, desc: desc) ?? []
  237. : medialib.audioFiles(with: sort, desc: desc) ?? []
  238. }
  239. @objc func fetchMedia(with mrl: URL?) -> VLCMLMedia? {
  240. guard let mrl = mrl else {
  241. assertionFailure("MedialibraryService: no mrl")
  242. return nil
  243. }
  244. return medialib.media(withMrl: mrl)
  245. }
  246. @objc func media(for identifier: VLCMLIdentifier) -> VLCMLMedia? {
  247. return medialib.media(withIdentifier: identifier)
  248. }
  249. func savePlaybackState(from player: VLCPlaybackController) {
  250. let media: VLCMedia? = player.currentlyPlayingMedia
  251. guard let mlMedia = fetchMedia(with: media?.url.absoluteURL) else {
  252. // we opened a url and not a local file
  253. return
  254. }
  255. mlMedia.isNew = false
  256. mlMedia.progress = player.playbackPosition
  257. mlMedia.audioTrackIndex = Int64(player.indexOfCurrentAudioTrack)
  258. mlMedia.subtitleTrackIndex = Int64(player.indexOfCurrentSubtitleTrack)
  259. mlMedia.chapterIndex = Int64(player.indexOfCurrentChapter)
  260. mlMedia.titleIndex = Int64(player.indexOfCurrentTitle)
  261. //create a new thumbnail
  262. }
  263. }
  264. // MARK: - Audio methods
  265. @objc extension MediaLibraryService {
  266. func artists(sortingCriteria sort: VLCMLSortingCriteria = .alpha,
  267. desc: Bool = false, listAll all: Bool = false) -> [VLCMLArtist] {
  268. return medialib.artists(with: sort, desc: desc, all: all) ?? []
  269. }
  270. func albums(sortingCriteria sort: VLCMLSortingCriteria = .alpha,
  271. desc: Bool = false) -> [VLCMLAlbum] {
  272. return medialib.albums(with: sort, desc: desc) ?? []
  273. }
  274. }
  275. // MARK: - Video methods
  276. extension MediaLibraryService {
  277. func requestThumbnail(for media: [VLCMLMedia]) {
  278. media.forEach() {
  279. guard !$0.isThumbnailGenerated() else { return }
  280. if !medialib.requestThumbnail(for: $0) {
  281. assertionFailure("MediaLibraryService: Failed to generate thumbnail for: \($0.identifier())")
  282. }
  283. }
  284. }
  285. }
  286. // MARK: - Playlist methods
  287. @objc extension MediaLibraryService {
  288. func createPlaylist(with name: String) -> VLCMLPlaylist? {
  289. return medialib.createPlaylist(withName: name)
  290. }
  291. func deletePlaylist(with identifier: VLCMLIdentifier) -> Bool {
  292. return medialib.deletePlaylist(withIdentifier: identifier)
  293. }
  294. func playlists(sortingCriteria sort: VLCMLSortingCriteria = .default,
  295. desc: Bool = false) -> [VLCMLPlaylist] {
  296. return medialib.playlists(with: sort, desc: desc) ?? []
  297. }
  298. }
  299. // MARK: - Genre methods
  300. extension MediaLibraryService {
  301. func genres(sortingCriteria sort: VLCMLSortingCriteria = .alpha,
  302. desc: Bool = false) -> [VLCMLGenre] {
  303. return medialib.genres(with: sort, desc: desc) ?? []
  304. }
  305. }
  306. // MARK: - VLCMediaFileDiscovererDelegate
  307. extension MediaLibraryService: VLCMediaFileDiscovererDelegate {
  308. func mediaFileAdded(_ filePath: String!, loading isLoading: Bool) {
  309. guard !isLoading else {
  310. return
  311. }
  312. /* exclude media files from backup (QA1719) */
  313. var excludeURL = URL(fileURLWithPath: filePath)
  314. var resourceValue = URLResourceValues()
  315. resourceValue.isExcludedFromBackup = true
  316. do {
  317. try excludeURL.setResourceValues(resourceValue)
  318. } catch let error {
  319. assertionFailure("MediaLibraryService: VLCMediaFileDiscovererDelegate: \(error.localizedDescription)")
  320. }
  321. reload()
  322. }
  323. func mediaFileDeleted(_ filePath: String!) {
  324. reload()
  325. }
  326. }
  327. // MARK: - VLCMediaLibraryDelegate - Media
  328. extension MediaLibraryService: VLCMediaLibraryDelegate {
  329. func medialibrary(_ medialibrary: VLCMediaLibrary, didAddMedia media: [VLCMLMedia]) {
  330. media.forEach { $0.updateCoreSpotlightEntry() }
  331. let videos = media.filter {( $0.type() == .video )}
  332. let tracks = media.filter {( $0.type() == .audio )}
  333. // thumbnails only for videos
  334. requestThumbnail(for: videos)
  335. for observer in observers {
  336. observer.value.observer?.medialibrary?(self, didAddVideos: videos)
  337. observer.value.observer?.medialibrary?(self, didAddTracks: tracks)
  338. }
  339. }
  340. func medialibrary(_ medialibrary: VLCMediaLibrary, didModifyMedia media: [VLCMLMedia]) {
  341. media.forEach { $0.updateCoreSpotlightEntry() }
  342. let showEpisodes = media.filter {( $0.subtype() == .showEpisode )}
  343. let albumTrack = media.filter {( $0.subtype() == .albumTrack )}
  344. for observer in observers {
  345. observer.value.observer?.medialibrary?(self, didAddShowEpisodes: showEpisodes)
  346. observer.value.observer?.medialibrary?(self, didAddAlbumTracks: albumTrack)
  347. }
  348. }
  349. func medialibrary(_ medialibrary: VLCMediaLibrary, didDeleteMediaWithIds mediaIds: [NSNumber]) {
  350. var stringIds = [String]()
  351. mediaIds.forEach { stringIds.append("\($0)") }
  352. CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: stringIds, completionHandler: nil)
  353. for observer in observers {
  354. observer.value.observer?.medialibrary?(self, didDeleteMediaWithIds: mediaIds)
  355. }
  356. }
  357. func medialibrary(_ medialibrary: VLCMediaLibrary, thumbnailReadyFor media: VLCMLMedia, withSuccess success: Bool) {
  358. for observer in observers {
  359. observer.value.observer?.medialibrary?(self, thumbnailReady: media)
  360. }
  361. }
  362. }
  363. // MARK: - VLCMediaLibraryDelegate - Artists
  364. extension MediaLibraryService {
  365. func medialibrary(_ medialibrary: VLCMediaLibrary, didAdd artists: [VLCMLArtist]) {
  366. for observer in observers {
  367. observer.value.observer?.medialibrary?(self, didAddArtists: artists)
  368. }
  369. }
  370. func medialibrary(_ medialibrary: VLCMediaLibrary, didDeleteArtistsWithIds artistsIds: [NSNumber]) {
  371. for observer in observers {
  372. observer.value.observer?.medialibrary?(self, didDeleteArtistsWithIds: artistsIds)
  373. }
  374. }
  375. }
  376. // MARK: - VLCMediaLibraryDelegate - Albums
  377. extension MediaLibraryService {
  378. func medialibrary(_ medialibrary: VLCMediaLibrary, didAdd albums: [VLCMLAlbum]) {
  379. for observer in observers {
  380. observer.value.observer?.medialibrary?(self, didAddAlbums: albums)
  381. }
  382. }
  383. func medialibrary(_ medialibrary: VLCMediaLibrary, didDeleteAlbumsWithIds albumsIds: [NSNumber]) {
  384. for observer in observers {
  385. observer.value.observer?.medialibrary?(self, didDeleteAlbumsWithIds: albumsIds)
  386. }
  387. }
  388. }
  389. // MARK: - VLCMediaLibraryDelegate - Playlists
  390. extension MediaLibraryService {
  391. func medialibrary(_ medialibrary: VLCMediaLibrary, didAdd playlists: [VLCMLPlaylist]) {
  392. for observer in observers {
  393. observer.value.observer?.medialibrary?(self, didAddPlaylists: playlists)
  394. }
  395. }
  396. func medialibrary(_ medialibrary: VLCMediaLibrary, didModifyPlaylists playlists: [VLCMLPlaylist]) {
  397. for observer in observers {
  398. observer.value.observer?.medialibrary?(self, didModifyPlaylists: playlists)
  399. }
  400. }
  401. func medialibrary(_ medialibrary: VLCMediaLibrary, didDeletePlaylistsWithIds playlistsIds: [NSNumber]) {
  402. for observer in observers {
  403. observer.value.observer?.medialibrary?(self, didDeletePlaylistsWithIds: playlistsIds)
  404. }
  405. }
  406. }
  407. // MARK: - VLCMediaLibraryDelegate - Discovery
  408. extension MediaLibraryService {
  409. func medialibrary(_ medialibrary: VLCMediaLibrary, didStartDiscovery entryPoint: String) {
  410. }
  411. func medialibrary(_ medialibrary: VLCMediaLibrary, didCompleteDiscovery entryPoint: String) {
  412. didFinishDiscovery = true
  413. }
  414. func medialibrary(_ medialibrary: VLCMediaLibrary, didProgressDiscovery entryPoint: String) {
  415. }
  416. func medialibrary(_ medialibrary: VLCMediaLibrary, didUpdateParsingStatsWithPercent percent: UInt32) {
  417. if didFinishDiscovery && percent == 100 {
  418. startMigrationIfNeeded()
  419. }
  420. }
  421. }