VLCMediaLibraryManager.swift 16.9 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12
/*****************************************************************************
 * VLCMediaLibraryManager.swift
 * VLC for iOS
 *****************************************************************************
 * Copyright © 2018 VideoLAN. All rights reserved.
 * Copyright © 2018 Videolabs
 *
 * Authors: Soomin Lee <bubu # mikan.io>
 *
 * Refer to the COPYING file of the official project for license.
 *****************************************************************************/

13 14 15 16 17 18 19 20 21
extension Notification.Name {
    static let VLCNewFileAddedNotification = Notification.Name("NewFileAddedNotification")
}

// For objc
extension NSNotification {
    @objc static let VLCNewFileAddedNotification = Notification.Name.VLCNewFileAddedNotification
}

22
@objc protocol MediaLibraryObserver: class {
23
    // Video
24
    @objc optional func medialibrary(_ medialibrary: VLCMediaLibraryManager,
25
                                     didModifyVideo video: [VLCMLMedia])
26 27

    @objc optional func medialibrary(_ medialibrary: VLCMediaLibraryManager,
28
                                     didDeleteMediaWithIds ids: [NSNumber])
29

30
    @objc optional func medialibrary(_ medialibrary: VLCMediaLibraryManager,
31
                                     didAddVideos videos: [VLCMLMedia])
32

33
    @objc optional func medialibrary(_ medialibrary: VLCMediaLibraryManager,
34
                                     didAddShowEpisodes showEpisodes: [VLCMLMedia])
35

36 37 38
    @objc optional func medialibrary(_ medialibrary: VLCMediaLibraryManager,
                                     thumbnailReady media: VLCMLMedia)

39 40
    // Audio
    @objc optional func medialibrary(_ medialibrary: VLCMediaLibraryManager,
41
                                     didAddAudios audios: [VLCMLMedia])
42 43

    @objc optional func medialibrary(_ medialibrary: VLCMediaLibraryManager,
44
                                     didAddArtists artists: [VLCMLArtist])
Soomin Lee's avatar
Soomin Lee committed
45 46

    @objc optional func medialibrary(_ medialibrary: VLCMediaLibraryManager,
47
                                     didDeleteArtistsWithIds artistsIds: [NSNumber])
Soomin Lee's avatar
Soomin Lee committed
48

Soomin Lee's avatar
Soomin Lee committed
49
    @objc optional func medialibrary(_ medialibrary: VLCMediaLibraryManager,
50
                                     didAddAlbums albums: [VLCMLAlbum])
Soomin Lee's avatar
Soomin Lee committed
51

52 53 54 55 56 57
    @objc optional func medialibrary(_ medialibrary: VLCMediaLibraryManager,
                                     didDeleteAlbumsWithIds albumsIds: [NSNumber])

    @objc optional func medialibrary(_ medialibrary: VLCMediaLibraryManager,
                                     didAddAlbumTracks albumTracks: [VLCMLMedia])

Soomin Lee's avatar
Soomin Lee committed
58
    @objc optional func medialibrary(_ medialibrary: VLCMediaLibraryManager,
59
                                     didAddGenres genres: [VLCMLGenre])
60 61 62 63

    // Playlist
    @objc optional func medialibrary(_ medialibrary: VLCMediaLibraryManager,
                                     didAddPlaylists playlists: [VLCMLPlaylist])
64 65 66

    @objc optional func medialibrary(_ medialibrary: VLCMediaLibraryManager,
                                     didDeletePlaylistsWithIds playlistsIds: [NSNumber])
67 68
}

69 70 71 72 73 74 75 76
protocol MediaLibraryMigrationDelegate: class {
    func medialibraryDidStartMigration(_ medialibrary: VLCMediaLibraryManager)

    func medialibraryDidFinishMigration(_ medialibrary: VLCMediaLibraryManager)

    func medialibraryDidStopMigration(_ medialibrary: VLCMediaLibraryManager)
}

77 78
class VLCMediaLibraryManager: NSObject {
    private static let databaseName: String = "medialibrary.db"
79 80 81
    private static let migrationKey: String = "MigratedToVLCMediaLibraryKit"

    private var didMigrate = UserDefaults.standard.bool(forKey: VLCMediaLibraryManager.migrationKey)
82

83 84 85 86
    // Using ObjectIdentifier to avoid duplication and facilitate
    // identification of observing object
    private var observers = [ObjectIdentifier: Observer]()

87
    private lazy var medialib: VLCMediaLibrary = {
88 89 90 91 92
        let medialibrary = VLCMediaLibrary()
        medialibrary.delegate = self
        return medialibrary
    }()

93 94
    weak var migrationDelegate: MediaLibraryMigrationDelegate?

95 96 97
    override init() {
        super.init()
        setupMediaLibrary()
98 99
        NotificationCenter.default.addObserver(self, selector: #selector(reload),
                                               name: .VLCNewFileAddedNotification, object: nil)
100 101 102
    }

    // MARK: Private
103

104 105 106 107 108 109 110
    private func setupMediaDiscovery(at path: String) {
        let mediaFileDiscoverer = VLCMediaFileDiscoverer.sharedInstance()
        mediaFileDiscoverer?.directoryPath = path
        mediaFileDiscoverer?.addObserver(self)
        mediaFileDiscoverer?.startDiscovering()
    }

111 112
    private func setupMediaLibrary() {
        guard let documentPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first,
113
            let libraryPath = NSSearchPathForDirectoriesInDomains(.libraryDirectory, .userDomainMask, true).first else {
114 115 116
                preconditionFailure("VLCMediaLibraryManager: Unable to init medialibrary.")
        }

117 118
        setupMediaDiscovery(at: documentPath)

119 120 121 122 123 124 125 126 127
        let databasePath = libraryPath + "/MediaLibrary/" + VLCMediaLibraryManager.databaseName
        let thumbnailPath = libraryPath + "/MediaLibrary/Thumbnails"

        do {
            try FileManager.default.createDirectory(atPath: thumbnailPath,
                                                    withIntermediateDirectories: true)
        } catch let error as NSError {
            assertionFailure("Failed to create directory: \(error.localizedDescription)")
        }
128

129 130
        let medialibraryStatus = medialib.setupMediaLibrary(databasePath: databasePath,
                                                            thumbnailPath: thumbnailPath)
131 132 133

        switch medialibraryStatus {
        case .success:
134
            guard medialib.start() else {
135 136 137
                assertionFailure("VLCMediaLibraryManager: Medialibrary failed to start.")
                return
            }
138 139
            medialib.reload()
            medialib.discover(onEntryPoint: "file://" + documentPath)
140 141 142 143 144 145 146 147 148 149 150 151
        case .alreadyInitialized:
            assertionFailure("VLCMediaLibraryManager: Medialibrary already initialized.")
        case .failed:
            preconditionFailure("VLCMediaLibraryManager: Failed to setup medialibrary.")
        case .dbReset:
            // should still start and discover but warn the user that the db has been wipped
            assertionFailure("VLCMediaLibraryManager: The database was resetted, please re-configure.")
        }
    }

    // MARK: Internal

152
    @objc func reload() {
153 154 155
        medialib.reload()
    }

156 157
    /// Returns number of *ALL* files(audio and video) present in the medialibrary database
    func numberOfFiles() -> Int {
158
        var media = medialib.audioFiles(with: .filename, desc: false)
159

160
        media += medialib.videoFiles(with: .filename, desc: false)
161 162 163 164 165 166 167 168
        return media.count
    }


    /// Returns *ALL* file found for a specified VLCMLMediaType
    ///
    /// - Parameter type: Type of the media
    /// - Returns: Array of VLCMLMedia
169 170
    func media(ofType type: VLCMLMediaType, sortingCriteria sort: VLCMLSortingCriteria = .filename, desc: Bool = false) -> [VLCMLMedia] {
        return type == .video ? medialib.videoFiles(with: sort, desc: desc) : medialib.audioFiles(with: sort, desc: desc)
171 172
    }

Soomin Lee's avatar
Soomin Lee committed
173 174 175
    func genre(sortingCriteria sort: VLCMLSortingCriteria = .default, desc: Bool = false) -> [VLCMLGenre] {
        return medialib.genres(with: sort, desc: desc)
    }
176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274

    func fetchMedia(with mrl: URL?) -> VLCMLMedia? {
        guard let mrl = mrl  else {
            return nil
        }
        return medialib.media(withMrl: mrl)
    }

    @discardableResult
    func prepareMigrationIfNeeded() -> Bool {
        if !didMigrate {
            migrationDelegate?.medialibraryDidStartMigration(self)
            return true
        }
        return false
    }
}

// MARK: - Migration

private extension VLCMediaLibraryManager {
    func startMigrationIfNeeded() {
        guard !didMigrate else {
            return
        }

        guard migrateToNewMediaLibrary() else {
            migrationDelegate?.medialibraryDidStopMigration(self)
            return
        }

        migrationDelegate?.medialibraryDidFinishMigration(self)
    }

    func migrateMedia(_ oldMedialibrary: MLMediaLibrary) -> Bool {
        guard let allFiles = MLFile.allFiles() as? [MLFile] else {
            assertionFailure("VLCMediaLibraryManager: Migration: Unable to retreive all files")
            return false
        }

        for media in allFiles {
            if let newMedia = fetchMedia(with: media.url) {
                newMedia.updateTitle(media.title)
                newMedia.setPlayCount(media.playCount.uint32Value)
                newMedia.setMetadataOf(.progress, intValue: media.lastPosition.int64Value)
                newMedia.setMetadataOf(.seen, intValue: media.unread.int64Value)
                // Only delete files that are not in playlist
                if media.labels.isEmpty {
                    oldMedialibrary.remove(media)
                }
            }
        }
        oldMedialibrary.save()
        return true
    }

    // This private method migrates old playlist and removes file and playlist
    // from the old medialibrary.
    // Note: This removes **only** files that are in a playlist
    func migratePlaylists(_ oldMedialibrary: MLMediaLibrary) -> Bool {
        guard let allLabels = MLLabel.allLabels() as? [MLLabel] else {
            assertionFailure("VLCMediaLibraryManager: Migration: Unable to retreive all labels")
            return false
        }

        for label in allLabels {
            let newPlaylist = createPlaylist(with: label.name)

            guard let files = label.files as? Set<MLFile> else {
                assertionFailure("VLCMediaLibraryManager: Migration: Unable to retreive files from label")
                oldMedialibrary.remove(label)
                continue
            }

            for file in files {
                if let newMedia = fetchMedia(with: file.url) {
                    if newPlaylist.appendMedia(withIdentifier: newMedia.identifier()) {
                        oldMedialibrary.remove(file)
                    }
                }
            }
            oldMedialibrary.remove(label)
        }
        oldMedialibrary.save()
        return true
    }

    func migrateToNewMediaLibrary() -> Bool {
        guard let oldMedialibrary = MLMediaLibrary.sharedMediaLibrary() as? MLMediaLibrary else {
            assertionFailure("VLCMediaLibraryManager: Migration: Unable to retreive old medialibrary")
            return false
        }

        if migrateMedia(oldMedialibrary) && migratePlaylists(oldMedialibrary) {
            UserDefaults.standard.set(true, forKey: VLCMediaLibraryManager.migrationKey)
            return true
        }
        return false
    }
275 276
}

277
// MARK: - Observer
278

279 280 281 282 283
private extension VLCMediaLibraryManager {
    struct Observer {
        weak var observer: MediaLibraryObserver?
    }
}
284 285

extension VLCMediaLibraryManager {
286 287
    func addObserver(_ observer: MediaLibraryObserver) {
        let identifier = ObjectIdentifier(observer)
288
        observers[identifier] = Observer(observer: observer)
289 290 291 292 293 294
    }

    func removeObserver(_ observer: MediaLibraryObserver) {
        let identifier = ObjectIdentifier(observer)
        observers.removeValue(forKey: identifier)
    }
295 296
}

297
// MARK: MediaLibrary - Audio methods
298 299

extension VLCMediaLibraryManager {
300 301
    func getArtists(sortingCriteria sort: VLCMLSortingCriteria = .artist, desc: Bool = false) -> [VLCMLArtist] {
        return medialib.artists(with: sort, desc: desc, all: true)
302 303
    }

304 305
    func getAlbums(sortingCriteria sort: VLCMLSortingCriteria = .album, desc: Bool = false) -> [VLCMLAlbum] {
        return medialib.albums(with: sort, desc: desc)
306 307 308
    }
}

309
// MARK: MediaLibrary - Video methods
310 311

extension VLCMediaLibraryManager {
312 313
    func requestThumbnail(for media: [VLCMLMedia]) {
        media.forEach() {
314
            guard !$0.isThumbnailGenerated() else { return }
315 316 317 318 319 320

            if !medialib.requestThumbnail(for: $0) {
                assertionFailure("VLCMediaLibraryManager: Failed to generate thumbnail for: \($0.identifier())")
            }
        }
    }
321 322
}

323 324 325
// MARK: MediaLibrary - Playlist methods

extension VLCMediaLibraryManager {
326 327 328 329 330 331 332 333 334

    func createPlaylist(with name: String) -> VLCMLPlaylist {
        return medialib.createPlaylist(withName: name)
    }

    func deletePlaylist(with identifier: VLCMLIdentifier) -> Bool {
        return medialib.deletePlaylist(withIdentifier: identifier)
    }

335 336 337 338 339
    func getPlaylists(sortingCriteria sort: VLCMLSortingCriteria = .default, desc: Bool = false) -> [VLCMLPlaylist] {
        return medialib.playlists(with: sort, desc: desc)
    }
}

340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364
extension VLCMediaLibraryManager: VLCMediaFileDiscovererDelegate {
    func mediaFileAdded(_ filePath: String!, loading isLoading: Bool) {
        guard !isLoading else {
            return
        }
        /* exclude media files from backup (QA1719) */
        var excludeURL = URL(fileURLWithPath: filePath)
        var resourceValue = URLResourceValues()

        resourceValue.isExcludedFromBackup = true

        do {
            try excludeURL.setResourceValues(resourceValue)
        } catch let error {
            assertionFailure("VLCMediaLibraryManager: VLCMediaFileDiscovererDelegate: \(error.localizedDescription)")
        }

        reload()
    }

    func mediaFileDeleted(_ filePath: String!) {
        reload()
    }
}

365 366
// MARK: - VLCMediaLibraryDelegate - Media

367 368
extension VLCMediaLibraryManager: VLCMediaLibraryDelegate {
    func medialibrary(_ medialibrary: VLCMediaLibrary, didAddMedia media: [VLCMLMedia]) {
369
        let videos = media.filter {( $0.type() == .video )}
370 371
        let audio = media.filter {( $0.type() == .audio )}

372
        // thumbnails only for videos
373
        requestThumbnail(for: videos)
374

375
        for observer in observers {
376 377
            observer.value.observer?.medialibrary?(self, didAddVideos: videos)
            observer.value.observer?.medialibrary?(self, didAddAudios: audio)
378 379 380
        }
    }

381
    func medialibrary(_ medialibrary: VLCMediaLibrary, didModifyMedia media: [VLCMLMedia]) {
382 383 384 385
        let showEpisodes = media.filter {( $0.subtype() == .showEpisode )}
        let albumTrack = media.filter {( $0.subtype() == .albumTrack )}

        for observer in observers {
386 387
            observer.value.observer?.medialibrary?(self, didAddShowEpisodes: showEpisodes)
            observer.value.observer?.medialibrary?(self, didAddAlbumTracks: albumTrack)
388
        }
389 390
    }

391 392 393 394 395
    func medialibrary(_ medialibrary: VLCMediaLibrary, didDeleteMediaWithIds mediaIds: [NSNumber]) {
        for observer in observers {
            observer.value.observer?.medialibrary?(self, didDeleteMediaWithIds: mediaIds)
        }
    }
396 397 398 399 400 401

    func medialibrary(_ medialibrary: VLCMediaLibrary, thumbnailReadyFor media: VLCMLMedia, withSuccess success: Bool) {
        for observer in observers {
            observer.value.observer?.medialibrary?(self, thumbnailReady: media)
        }
    }
402 403 404
}

// MARK: - VLCMediaLibraryDelegate - Artists
405

406
extension VLCMediaLibraryManager {
Soomin Lee's avatar
Soomin Lee committed
407 408
    func medialibrary(_ medialibrary: VLCMediaLibrary, didAdd artists: [VLCMLArtist]) {
        for observer in observers {
409
            observer.value.observer?.medialibrary?(self, didAddArtists: artists)
Soomin Lee's avatar
Soomin Lee committed
410 411
        }
    }
412 413 414 415 416 417

    func medialibrary(_ medialibrary: VLCMediaLibrary, didDeleteArtistsWithIds artistsIds: [NSNumber]) {
        for observer in observers {
            observer.value.observer?.medialibrary?(self, didDeleteArtistsWithIds: artistsIds)
        }
    }
418
}
Soomin Lee's avatar
Soomin Lee committed
419

420 421 422
// MARK: - VLCMediaLibraryDelegate - Albums

extension VLCMediaLibraryManager {
Soomin Lee's avatar
Soomin Lee committed
423 424
    func medialibrary(_ medialibrary: VLCMediaLibrary, didAdd albums: [VLCMLAlbum]) {
        for observer in observers {
425
            observer.value.observer?.medialibrary?(self, didAddAlbums: albums)
Soomin Lee's avatar
Soomin Lee committed
426 427
        }
    }
428 429 430 431 432 433

    func medialibrary(_ medialibrary: VLCMediaLibrary, didDeleteAlbumsWithIds albumsIds: [NSNumber]) {
        for observer in observers {
            observer.value.observer?.medialibrary?(self, didDeleteAlbumsWithIds: albumsIds)
        }
    }
434
}
Soomin Lee's avatar
Soomin Lee committed
435

436 437 438 439 440 441 442 443
// MARK: - VLCMediaLibraryDelegate - Playlists

extension VLCMediaLibraryManager {
    func medialibrary(_ medialibrary: VLCMediaLibrary, didAdd playlists: [VLCMLPlaylist]) {
        for observer in observers {
            observer.value.observer?.medialibrary?(self, didAddPlaylists: playlists)
        }
    }
444 445 446 447 448 449

    func medialibrary(_ medialibrary: VLCMediaLibrary, didDeletePlaylistsWithIds playlistsIds: [NSNumber]) {
        for observer in observers {
            observer.value.observer?.medialibrary?(self, didDeletePlaylistsWithIds: playlistsIds)
        }
    }
450 451
}

452 453 454
// MARK: - VLCMediaLibraryDelegate - Discovery

extension VLCMediaLibraryManager {
455 456 457 458
    func medialibrary(_ medialibrary: VLCMediaLibrary, didStartDiscovery entryPoint: String) {
    }

    func medialibrary(_ medialibrary: VLCMediaLibrary, didCompleteDiscovery entryPoint: String) {
459
        startMigrationIfNeeded()
460 461 462 463 464 465 466 467
    }

    func medialibrary(_ medialibrary: VLCMediaLibrary, didProgressDiscovery entryPoint: String) {
    }

    func medialibrary(_ medialibrary: VLCMediaLibrary, didUpdateParsingStatsWithPercent percent: UInt32) {
    }
}