MediaLibraryService.swift 23.3 KB
Newer Older
1
/*****************************************************************************
2
 * MediaLibraryService.swift
3 4 5 6 7 8 9 10 11 12
 * 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
// MARK: - Notification names

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

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

24 25
// MARK: -

26
@objc protocol MediaLibraryObserver: class {
27
    // Video
28 29
    @objc optional func medialibrary(_ medialibrary: MediaLibraryService,
                                     didAddVideos videos: [VLCMLMedia])
30

31
    @objc optional func medialibrary(_ medialibrary: MediaLibraryService,
32
                                     didModifyVideos videos: [VLCMLMedia])
33

34
    @objc optional func medialibrary(_ medialibrary: MediaLibraryService,
35
                                     didDeleteMediaWithIds ids: [NSNumber])
36

37
    // ShowEpisodes
38
    @objc optional func medialibrary(_ medialibrary: MediaLibraryService,
39
                                     didAddShowEpisodes showEpisodes: [VLCMLMedia])
40

41
    // Tumbnail
42
    @objc optional func medialibrary(_ medialibrary: MediaLibraryService,
43 44
                                     thumbnailReady media: VLCMLMedia)

45
    // Tracks
46
    @objc optional func medialibrary(_ medialibrary: MediaLibraryService,
47
                                     didAddTracks tracks: [VLCMLMedia])
48

49 50 51
    @objc optional func medialibrary(_ medialibrary: MediaLibraryService,
                                     didModifyTracks tracks: [VLCMLMedia])

52
    // Artists
53
    @objc optional func medialibrary(_ medialibrary: MediaLibraryService,
54
                                     didAddArtists artists: [VLCMLArtist])
Soomin Lee's avatar
Soomin Lee committed
55

56 57 58
    @objc optional func medialibrary(_ medialibrary: MediaLibraryService,
                                     didModifyArtists artists: [VLCMLArtist])

59
    @objc optional func medialibrary(_ medialibrary: MediaLibraryService,
60
                                     didDeleteArtistsWithIds artistsIds: [NSNumber])
Soomin Lee's avatar
Soomin Lee committed
61

62
    // Albums
63
    @objc optional func medialibrary(_ medialibrary: MediaLibraryService,
64
                                     didAddAlbums albums: [VLCMLAlbum])
Soomin Lee's avatar
Soomin Lee committed
65

66 67 68
    @objc optional func medialibrary(_ medialibrary: MediaLibraryService,
                                     didModifyAlbums albums: [VLCMLAlbum])

69
    @objc optional func medialibrary(_ medialibrary: MediaLibraryService,
70 71
                                     didDeleteAlbumsWithIds albumsIds: [NSNumber])

72
    // AlbumTracks
73
    @objc optional func medialibrary(_ medialibrary: MediaLibraryService,
74 75
                                     didAddAlbumTracks albumTracks: [VLCMLMedia])

76
    // Genres
77
    @objc optional func medialibrary(_ medialibrary: MediaLibraryService,
78
                                     didAddGenres genres: [VLCMLGenre])
79

80 81 82
    @objc optional func medialibrary(_ medialibrary: MediaLibraryService,
                                     didModifyGenres genres: [VLCMLGenre])

83 84 85
    @objc optional func medialibrary(_ medialibrary: MediaLibraryService,
                                     didDeleteGenresWithIds genreIds: [NSNumber])

86
    // Playlist
87
    @objc optional func medialibrary(_ medialibrary: MediaLibraryService,
88
                                     didAddPlaylists playlists: [VLCMLPlaylist])
89

Carola Nitz's avatar
Carola Nitz committed
90 91 92
    @objc optional func medialibrary(_ medialibrary: MediaLibraryService,
                                     didModifyPlaylists playlists: [VLCMLPlaylist])

93
    @objc optional func medialibrary(_ medialibrary: MediaLibraryService,
94
                                     didDeletePlaylistsWithIds playlistsIds: [NSNumber])
95 96
}

97 98
// MARK: -

99
protocol MediaLibraryMigrationDelegate: class {
100
    func medialibraryDidStartMigration(_ medialibrary: MediaLibraryService)
101

102
    func medialibraryDidFinishMigration(_ medialibrary: MediaLibraryService)
103

104
    func medialibraryDidStopMigration(_ medialibrary: MediaLibraryService)
105 106
}

107 108
// MARK: -

109
class MediaLibraryService: NSObject {
110
    private static let databaseName: String = "medialibrary.db"
111 112
    private static let migrationKey: String = "MigratedToVLCMediaLibraryKit"

113
    private var didMigrate = UserDefaults.standard.bool(forKey: MediaLibraryService.migrationKey)
114
    private var didFinishDiscovery = false
115 116 117 118
    // Using ObjectIdentifier to avoid duplication and facilitate
    // identification of observing object
    private var observers = [ObjectIdentifier: Observer]()

119
    private lazy var medialib = VLCMediaLibrary()
120

121 122
    weak var migrationDelegate: MediaLibraryMigrationDelegate?

123 124
    override init() {
        super.init()
125
        medialib.delegate = self
126
        setupMediaLibrary()
127 128
        NotificationCenter.default.addObserver(self, selector: #selector(reload),
                                               name: .VLCNewFileAddedNotification, object: nil)
129 130 131

        NotificationCenter.default.addObserver(self, selector: #selector(handleWillEnterForegroundNotification),
                                               name: UIApplication.willEnterForegroundNotification, object: nil)
132
    }
133
}
134

135
// MARK: - Private initializers
136

137
private extension MediaLibraryService {
138 139 140 141 142 143 144
    private func setupMediaDiscovery(at path: String) {
        let mediaFileDiscoverer = VLCMediaFileDiscoverer.sharedInstance()
        mediaFileDiscoverer?.directoryPath = path
        mediaFileDiscoverer?.addObserver(self)
        mediaFileDiscoverer?.startDiscovering()
    }

145 146
    private func setupMediaLibrary() {
        guard let documentPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first,
147
            let libraryPath = NSSearchPathForDirectoriesInDomains(.libraryDirectory, .userDomainMask, true).first else {
148
                preconditionFailure("MediaLibraryService: Unable to init medialibrary.")
149 150
        }

151 152
        setupMediaDiscovery(at: documentPath)

153
        let databasePath = libraryPath + "/MediaLibrary/" + MediaLibraryService.databaseName
154 155 156 157 158 159 160 161
        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)")
        }
162

163 164
        let medialibraryStatus = medialib.setupMediaLibrary(databasePath: databasePath,
                                                            thumbnailPath: thumbnailPath)
165 166

        switch medialibraryStatus {
167
        case .success, .dbReset:
168
            guard medialib.start() else {
169
                assertionFailure("MediaLibraryService: Medialibrary failed to start.")
170 171
                return
            }
172 173
            medialib.reload()
            medialib.discover(onEntryPoint: "file://" + documentPath)
174
        case .alreadyInitialized:
175
            assertionFailure("MediaLibraryService: Medialibrary already initialized.")
176
        case .failed:
177
            preconditionFailure("MediaLibraryService: Failed to setup medialibrary.")
178 179
        @unknown default:
            assertionFailure("MediaLibraryService: unhandled case")
180 181
        }
    }
182 183 184 185
}

// MARK: - Migration

186
private extension MediaLibraryService {
187 188 189 190
    func startMigrationIfNeeded() {
        guard !didMigrate else {
            return
        }
191
        migrationDelegate?.medialibraryDidStartMigration(self)
192

193 194 195 196 197 198 199 200
        migrateToNewMediaLibrary() {
            [unowned self] success in
            if success {
                self.migrationDelegate?.medialibraryDidFinishMigration(self)
            } else {
                self.migrationDelegate?.medialibraryDidStopMigration(self)
            }
        }
201 202
    }

203 204
    func migrateMedia(_ oldMedialibrary: MLMediaLibrary,
                      completionHandler: @escaping (Bool) -> Void) {
205
        guard let allFiles = MLFile.allFiles() as? [MLFile] else {
Felix Paul Kühne's avatar
Felix Paul Kühne committed
206
            assertionFailure("MediaLibraryService: Migration: Unable to retrieve all files")
207 208
            completionHandler(false)
            return
209 210 211 212 213 214 215 216 217 218 219 220 221 222
        }

        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)
                }
            }
        }
223 224 225 226 227

        oldMedialibrary.save {
            success in
            completionHandler(success)
        }
228 229 230 231 232
    }

    // 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
233 234
    func migratePlaylists(_ oldMedialibrary: MLMediaLibrary,
                          completionHandler: @escaping (Bool) -> Void) {
235
        guard let allLabels = MLLabel.allLabels() as? [MLLabel] else {
Felix Paul Kühne's avatar
Felix Paul Kühne committed
236
            assertionFailure("MediaLibraryService: Migration: Unable to retrieve all labels")
237 238
            completionHandler(false)
            return
239 240 241
        }

        for label in allLabels {
242 243 244 245
            guard let newPlaylist = createPlaylist(with: label.name) else {
                assertionFailure("MediaLibraryService: Migration: Unable to create playlist.")
                continue
            }
246 247

            guard let files = label.files as? Set<MLFile> else {
Felix Paul Kühne's avatar
Felix Paul Kühne committed
248
                assertionFailure("MediaLibraryService: Migration: Unable to retrieve files from label")
249 250 251 252 253 254 255 256 257 258 259 260 261
                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)
        }
262 263 264 265
        oldMedialibrary.save() {
            success in
            completionHandler(success)
        }
266 267
    }

268
    func migrateToNewMediaLibrary(completionHandler: @escaping (Bool) -> Void) {
269
        guard let oldMedialibrary = MLMediaLibrary.sharedMediaLibrary() as? MLMediaLibrary else {
Felix Paul Kühne's avatar
Felix Paul Kühne committed
270
            assertionFailure("MediaLibraryService: Migration: Unable to retrieve old medialibrary")
271 272
            completionHandler(false)
            return
273 274
        }

275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293
        migrateMedia(oldMedialibrary) {
            [unowned self] success in

            guard success else {
                assertionFailure("MediaLibraryService: Failed to migrate Media.")
                completionHandler(false)
                return
            }

            self.migratePlaylists(oldMedialibrary) {
                [unowned self] success in
                if success {
                    UserDefaults.standard.set(true, forKey: MediaLibraryService.migrationKey)
                    self.didMigrate = true
                } else {
                    assertionFailure("MediaLibraryService: Failed to migrate Playlist.")
                }
                completionHandler(success)
            }
294 295
        }
    }
296 297
}

298
// MARK: - Observer
299

300
private extension MediaLibraryService {
301 302 303 304
    struct Observer {
        weak var observer: MediaLibraryObserver?
    }
}
305

306
extension MediaLibraryService {
307 308
    func addObserver(_ observer: MediaLibraryObserver) {
        let identifier = ObjectIdentifier(observer)
309
        observers[identifier] = Observer(observer: observer)
310 311 312 313 314 315
    }

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

318 319
// MARK: - Helpers

320
@objc extension MediaLibraryService {
321 322 323 324
    @objc func reload() {
        medialib.reload()
    }

325 326 327 328
    @objc func reindexAllMediaForSpotlight() {
        media(ofType: .video).forEach { $0.updateCoreSpotlightEntry() }
        media(ofType: .audio).forEach { $0.updateCoreSpotlightEntry() }
    }
329 330
    /// Returns number of *ALL* files(audio and video) present in the medialibrary database
    func numberOfFiles() -> Int {
331
        return (medialib.audioFiles()?.count ?? 0) + (medialib.videoFiles()?.count ?? 0)
332 333 334 335 336 337
    }

    /// Returns *ALL* file found for a specified VLCMLMediaType
    ///
    /// - Parameter type: Type of the media
    /// - Returns: Array of VLCMLMedia
338
    func media(ofType type: VLCMLMediaType,
339
               sortingCriteria sort: VLCMLSortingCriteria = .alpha,
340 341 342
               desc: Bool = false) -> [VLCMLMedia] {
        return type == .video ? medialib.videoFiles(with: sort, desc: desc) ?? []
                              : medialib.audioFiles(with: sort, desc: desc) ?? []
343 344
    }

Carola Nitz's avatar
Carola Nitz committed
345
    @objc func fetchMedia(with mrl: URL?) -> VLCMLMedia? {
346
        guard let mrl = mrl  else {
347
            return nil //Happens when we have a URL or there is no currently playing file
348 349 350
        }
        return medialib.media(withMrl: mrl)
    }
Carola Nitz's avatar
Carola Nitz committed
351

352

Carola Nitz's avatar
Carola Nitz committed
353 354 355
    @objc func media(for identifier: VLCMLIdentifier) -> VLCMLMedia? {
        return medialib.media(withIdentifier: identifier)
    }
356

357
    func savePlaybackState(from player: PlaybackService) {
358

359 360
        let media: VLCMedia? = player.currentlyPlayingMedia
        guard let mlMedia = fetchMedia(with: media?.url.absoluteURL) else {
361 362 363 364 365 366 367 368 369 370 371 372
            // we opened a url and not a local file
            return
        }

        mlMedia.isNew = false
        mlMedia.progress = player.playbackPosition
        mlMedia.audioTrackIndex = Int64(player.indexOfCurrentAudioTrack)
        mlMedia.subtitleTrackIndex = Int64(player.indexOfCurrentSubtitleTrack)
        mlMedia.chapterIndex = Int64(player.indexOfCurrentChapter)
        mlMedia.titleIndex = Int64(player.indexOfCurrentTitle)
        //create a new thumbnail
    }
373 374 375 376 377 378 379 380 381 382 383 384 385 386 387

    private func handleWillEnterForegroundNotification() {
        guard let documentPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first else {
            assertionFailure("MediaLibraryService: handleWillEnterForegroundNotification: Failed to retrieve documentPath")
            return
        }
        // On each foreground notification we check if there is a `.Trash` folder which is invisible
        // for the user that can be created by deleting media from the Files app.
        // This could lead to disk space issues.
        // For now since we do not handle restoration, we delete the `.Trash` folder every time.
         _ = try? FileManager.default.removeItem(atPath: documentPath + "/.Trash")

        // Reload in order to make sure that there is no old artifacts left
        reload()
    }
388 389 390
}

// MARK: - Audio methods
391

392
@objc extension MediaLibraryService {
393
    func artists(sortingCriteria sort: VLCMLSortingCriteria = .alpha,
394 395
                 desc: Bool = false, listAll all: Bool = false) -> [VLCMLArtist] {
        return medialib.artists(with: sort, desc: desc, all: all) ?? []
396 397
    }

398 399
    func albums(sortingCriteria sort: VLCMLSortingCriteria = .alpha,
                desc: Bool = false) -> [VLCMLAlbum] {
400
        return medialib.albums(with: sort, desc: desc) ?? []
401 402 403
    }
}

404
// MARK: - Video methods
405

406
extension MediaLibraryService {
407 408 409 410 411 412 413 414 415 416
    func requestThumbnail(for media: VLCMLMedia) {
        if media.isThumbnailGenerated() || media.thumbnail() != nil {
            return
        }

        if !media.requestThumbnail(of: .thumbnail, desiredWidth: 320, desiredHeight: 200, atPosition: 0.03) {
            assertionFailure("MediaLibraryService: Failed to generate thumbnail for: \(media.identifier())")
        }
    }

417 418
    func requestThumbnail(for media: [VLCMLMedia]) {
        media.forEach() {
419
            requestThumbnail(for: $0)
420 421
        }
    }
422 423
}

424
// MARK: - Playlist methods
425

426
@objc extension MediaLibraryService {
427
    func createPlaylist(with name: String) -> VLCMLPlaylist? {
428 429 430 431 432 433 434
        return medialib.createPlaylist(withName: name)
    }

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

435 436
    func playlists(sortingCriteria sort: VLCMLSortingCriteria = .default,
                   desc: Bool = false) -> [VLCMLPlaylist] {
437
        return medialib.playlists(with: sort, desc: desc) ?? []
438 439 440
    }
}

441 442 443
// MARK: - Genre methods

extension MediaLibraryService {
444 445
    func genres(sortingCriteria sort: VLCMLSortingCriteria = .alpha,
                desc: Bool = false) -> [VLCMLGenre] {
446
        return medialib.genres(with: sort, desc: desc) ?? []
447 448 449 450 451
    }
}

// MARK: - VLCMediaFileDiscovererDelegate

452
extension MediaLibraryService: VLCMediaFileDiscovererDelegate {
453 454 455 456 457 458 459 460 461 462 463 464 465
    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 {
466
            assertionFailure("MediaLibraryService: VLCMediaFileDiscovererDelegate: \(error.localizedDescription)")
467 468 469 470 471 472 473 474 475 476
        }

        reload()
    }

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

477 478
// MARK: - VLCMediaLibraryDelegate - Media

479
extension MediaLibraryService: VLCMediaLibraryDelegate {
480
    func medialibrary(_ medialibrary: VLCMediaLibrary, didAddMedia media: [VLCMLMedia]) {
Carola Nitz's avatar
Carola Nitz committed
481 482 483

        media.forEach { $0.updateCoreSpotlightEntry() }

484
        let videos = media.filter {( $0.type() == .video )}
485
        let tracks = media.filter {( $0.type() == .audio )}
486

487
        for observer in observers {
488
            observer.value.observer?.medialibrary?(self, didAddVideos: videos)
489
            observer.value.observer?.medialibrary?(self, didAddTracks: tracks)
490 491 492
        }
    }

493
    func medialibrary(_ medialibrary: VLCMediaLibrary, didModifyMedia media: [VLCMLMedia]) {
Carola Nitz's avatar
Carola Nitz committed
494 495 496

        media.forEach { $0.updateCoreSpotlightEntry() }

497 498
        let showEpisodes = media.filter {( $0.subtype() == .showEpisode )}
        let albumTrack = media.filter {( $0.subtype() == .albumTrack )}
499 500
        let videos = media.filter {( $0.type() == .video)}
        let tracks = media.filter {( $0.type() == .audio)}
501

502
        // Shows and albumtracks are known only after when the medialibrary calls didModifyMedia
503
        for observer in observers {
504 505
            observer.value.observer?.medialibrary?(self, didAddShowEpisodes: showEpisodes)
            observer.value.observer?.medialibrary?(self, didAddAlbumTracks: albumTrack)
506 507
            observer.value.observer?.medialibrary?(self, didModifyVideos: videos)
            observer.value.observer?.medialibrary?(self, didModifyTracks: tracks)
508
        }
509 510
    }

511
    func medialibrary(_ medialibrary: VLCMediaLibrary, didDeleteMediaWithIds mediaIds: [NSNumber]) {
Carola Nitz's avatar
Carola Nitz committed
512 513 514 515 516

        var stringIds = [String]()
        mediaIds.forEach { stringIds.append("\($0)") }
        CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: stringIds, completionHandler: nil)

517 518 519 520
        for observer in observers {
            observer.value.observer?.medialibrary?(self, didDeleteMediaWithIds: mediaIds)
        }
    }
521 522 523 524 525 526

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

// MARK: - VLCMediaLibraryDelegate - Artists
530

531
extension MediaLibraryService {
Soomin Lee's avatar
Soomin Lee committed
532 533
    func medialibrary(_ medialibrary: VLCMediaLibrary, didAdd artists: [VLCMLArtist]) {
        for observer in observers {
534
            observer.value.observer?.medialibrary?(self, didAddArtists: artists)
Soomin Lee's avatar
Soomin Lee committed
535 536
        }
    }
537

538 539 540 541 542 543
    func medialibrary(_ medialibrary: VLCMediaLibrary, didModifyArtists artists: [VLCMLArtist]) {
        for observer in observers {
            observer.value.observer?.medialibrary?(self, didModifyArtists: artists)
        }
    }

544 545 546 547 548
    func medialibrary(_ medialibrary: VLCMediaLibrary, didDeleteArtistsWithIds artistsIds: [NSNumber]) {
        for observer in observers {
            observer.value.observer?.medialibrary?(self, didDeleteArtistsWithIds: artistsIds)
        }
    }
549
}
Soomin Lee's avatar
Soomin Lee committed
550

551 552
// MARK: - VLCMediaLibraryDelegate - Albums

553
extension MediaLibraryService {
Soomin Lee's avatar
Soomin Lee committed
554 555
    func medialibrary(_ medialibrary: VLCMediaLibrary, didAdd albums: [VLCMLAlbum]) {
        for observer in observers {
556
            observer.value.observer?.medialibrary?(self, didAddAlbums: albums)
Soomin Lee's avatar
Soomin Lee committed
557 558
        }
    }
559

560 561
    func medialibrary(_ medialibrary: VLCMediaLibrary, didModify albums: [VLCMLAlbum]) {
        for observer in observers {
562
            observer.value.observer?.medialibrary?(self, didModifyAlbums: albums)
563 564 565
        }
    }

566 567 568 569 570
    func medialibrary(_ medialibrary: VLCMediaLibrary, didDeleteAlbumsWithIds albumsIds: [NSNumber]) {
        for observer in observers {
            observer.value.observer?.medialibrary?(self, didDeleteAlbumsWithIds: albumsIds)
        }
    }
571
}
Soomin Lee's avatar
Soomin Lee committed
572

573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594
// MARK: - VLCMediaLibraryDelegate - Genres
extension MediaLibraryService {
    func medialibrary(_ medialibrary: VLCMediaLibrary, didAdd genres: [VLCMLGenre]) {
        for observer in observers {
            observer.value.observer?.medialibrary?(self, didAddGenres: genres)
        }
    }

    func medialibrary(_ medialibrary: VLCMediaLibrary, didModifyGenres genres: [VLCMLGenre]) {
        for observer in observers {
            observer.value.observer?.medialibrary?(self, didModifyGenres: genres)
        }
    }

    func medialibrary(_ medialibrary: VLCMediaLibrary,
                      didDeleteGenresWithIds genresIds: [NSNumber]) {
        for observer in observers {
            observer.value.observer?.medialibrary?(self, didDeleteGenresWithIds: genresIds)
        }
    }
}

595 596
// MARK: - VLCMediaLibraryDelegate - Playlists

597
extension MediaLibraryService {
598 599 600 601 602
    func medialibrary(_ medialibrary: VLCMediaLibrary, didAdd playlists: [VLCMLPlaylist]) {
        for observer in observers {
            observer.value.observer?.medialibrary?(self, didAddPlaylists: playlists)
        }
    }
603

Carola Nitz's avatar
Carola Nitz committed
604 605 606 607 608 609
    func medialibrary(_ medialibrary: VLCMediaLibrary, didModifyPlaylists playlists: [VLCMLPlaylist]) {
        for observer in observers {
            observer.value.observer?.medialibrary?(self, didModifyPlaylists: playlists)
        }
    }

610 611 612 613 614
    func medialibrary(_ medialibrary: VLCMediaLibrary, didDeletePlaylistsWithIds playlistsIds: [NSNumber]) {
        for observer in observers {
            observer.value.observer?.medialibrary?(self, didDeletePlaylistsWithIds: playlistsIds)
        }
    }
615 616
}

617 618
// MARK: - VLCMediaLibraryDelegate - Discovery

619
extension MediaLibraryService {
620 621 622 623
    func medialibrary(_ medialibrary: VLCMediaLibrary, didStartDiscovery entryPoint: String) {
    }

    func medialibrary(_ medialibrary: VLCMediaLibrary, didCompleteDiscovery entryPoint: String) {
624
        didFinishDiscovery = true
625 626 627 628 629 630
    }

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

    func medialibrary(_ medialibrary: VLCMediaLibrary, didUpdateParsingStatsWithPercent percent: UInt32) {
631 632 633
        if didFinishDiscovery && percent == 100 {
             startMigrationIfNeeded()
        }
634 635
    }
}