/***************************************************************************** * VLCMediaLibraryManager.swift * VLC for iOS ***************************************************************************** * Copyright © 2018 VideoLAN. All rights reserved. * Copyright © 2018 Videolabs * * Authors: Soomin Lee * * Refer to the COPYING file of the official project for license. *****************************************************************************/ extension Notification.Name { static let VLCNewFileAddedNotification = Notification.Name("NewFileAddedNotification") } // For objc extension NSNotification { @objc static let VLCNewFileAddedNotification = Notification.Name.VLCNewFileAddedNotification } @objc protocol MediaLibraryObserver: class { @objc optional func medialibrary(_ medialibrary: VLCMediaLibraryManager, didUpdateVideo video: [VLCMLMedia]) @objc optional func medialibrary(_ medialibrary: VLCMediaLibraryManager, didAddVideo video: [VLCMLMedia]) @objc optional func medialibrary(_ medialibrary: VLCMediaLibraryManager, didAddShowEpisode showEpisode: [VLCMLMedia]) // Audio @objc optional func medialibrary(_ medialibrary: VLCMediaLibraryManager, didAddAudio audio: [VLCMLMedia]) @objc optional func medialibrary(_ medialibrary: VLCMediaLibraryManager, didAddAlbumTrack audio: [VLCMLMedia]) @objc optional func medialibrary(_ medialibrary: VLCMediaLibraryManager, didAddArtist artist: [VLCMLArtist]) @objc optional func medialibrary(_ medialibrary: VLCMediaLibraryManager, didAddAlbum album: [VLCMLAlbum]) @objc optional func medialibrary(_ medialibrary: VLCMediaLibraryManager, didAddGenre genre: [VLCMLGenre]) } class VLCMediaLibraryManager: NSObject { private static let databaseName: String = "medialibrary.db" private var databasePath: String! private var thumbnailPath: String! // Using ObjectIdentifier to avoid duplication and facilitate // identification of observing object private var observers = [ObjectIdentifier: Observer]() private lazy var medialib: VLCMediaLibrary = { let medialibrary = VLCMediaLibrary() medialibrary.delegate = self return medialibrary }() override init() { super.init() setupMediaLibrary() NotificationCenter.default.addObserver(self, selector: #selector(reload), name: .VLCNewFileAddedNotification, object: nil) } // MARK: Private private func setupMediaLibrary() { guard let documentPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first, let dbPath = NSSearchPathForDirectoriesInDomains(.libraryDirectory, .userDomainMask, true).first else { preconditionFailure("VLCMediaLibraryManager: Unable to init medialibrary.") } databasePath = dbPath + "/" + VLCMediaLibraryManager.databaseName thumbnailPath = documentPath let medialibraryStatus = medialib.setupMediaLibrary(databasePath: databasePath, thumbnailPath: thumbnailPath) switch medialibraryStatus { case .success: guard medialib.start() else { assertionFailure("VLCMediaLibraryManager: Medialibrary failed to start.") return } medialib.reload() medialib.discover(onEntryPoint: "file://" + documentPath) break case .alreadyInitialized: assertionFailure("VLCMediaLibraryManager: Medialibrary already initialized.") break case .failed: preconditionFailure("VLCMediaLibraryManager: Failed to setup medialibrary.") break 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.") break } } // MARK: Internal @objc private func reload() { medialib.reload() } /// Returns number of *ALL* files(audio and video) present in the medialibrary database func numberOfFiles() -> Int { var media = medialib.audioFiles(with: .filename, desc: false) media += medialib.videoFiles(with: .filename, desc: false) return media.count } /// Returns *ALL* file found for a specified VLCMLMediaType /// /// - Parameter type: Type of the media /// - Returns: Array of VLCMLMedia 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) } func genre(sortingCriteria sort: VLCMLSortingCriteria = .default, desc: Bool = false) -> [VLCMLGenre] { return medialib.genres(with: sort, desc: desc) } } // MARK: - Observer private extension VLCMediaLibraryManager { struct Observer { weak var observer: MediaLibraryObserver? init(_ observer: MediaLibraryObserver) { self.observer = observer } } } extension VLCMediaLibraryManager { func addObserver(_ observer: MediaLibraryObserver) { let identifier = ObjectIdentifier(observer) observers[identifier] = Observer(observer) } func removeObserver(_ observer: MediaLibraryObserver) { let identifier = ObjectIdentifier(observer) observers.removeValue(forKey: identifier) } } // MARK: MediaDataSource - Audio methods extension VLCMediaLibraryManager { private func getAllAudio() { // foundAudio = medialibrary.media(ofType: .audio) // artistsFromAudio() // albumsFromAudio() // audioPlaylistsFromAudio() // genresFromAudio() } func getArtists() -> [VLCMLArtist] { return medialib.artists(with: .artist, desc: false, all: true) } func getAlbums() -> [VLCMLAlbum] { return medialib.albums(with: .album, desc: false) } private func getAudioPlaylists() { // let labels = MLLabel.allLabels() as! [MLLabel] // audioPlaylist = labels.filter { // let audioFiles = $0.files.filter { // if let file = $0 as? MLFile { // return file.isSupportedAudioFile() // } // return false // } // return !audioFiles.isEmpty // } } private func genresFromAudio() { // let albumtracks = MLAlbumTrack.allTracks() as! [MLAlbumTrack] // let tracksWithArtist = albumtracks.filter { $0.genre != nil && $0.genre != "" } // genres = tracksWithArtist.map { $0.genre } } } // MARK: MediaDataSource - Video methods extension VLCMediaLibraryManager { private func getAllVideos() { // moviesFromVideos() // episodesFromVideos() // videoPlaylistsFromVideos() } private func getMovies() { // movies = foundVideos.filter { $0.subtype() == .movie } } private func getShowEpisodes() { // episodes = foundVideos.filter { $0.subtype() == .showEpisode } } private func getVideoPlaylists() { // let labels = MLLabel.allLabels() as! [MLLabel] // audioPlaylist = labels.filter { // let audioFiles = $0.files.filter { // if let file = $0 as? MLFile { // return file.isShowEpisode() || file.isMovie() || file.isClip() // } // return false // } // return !audioFiles.isEmpty // } } } // MARK: VLCMediaLibraryDelegate extension VLCMediaLibraryManager: VLCMediaLibraryDelegate { func medialibrary(_ medialibrary: VLCMediaLibrary, didAddMedia media: [VLCMLMedia]) { let video = media.filter {( $0.type() == .video )} let audio = media.filter {( $0.type() == .audio )} let showEpisode = media.filter {( $0.subtype() == .showEpisode )} let albumTrack = media.filter {( $0.subtype() == .albumTrack )} for observer in observers { observer.value.observer?.medialibrary?(self, didAddVideo: video) observer.value.observer?.medialibrary?(self, didAddAudio: audio) observer.value.observer?.medialibrary?(self, didAddShowEpisode: showEpisode) observer.value.observer?.medialibrary?(self, didAddAlbumTrack: albumTrack) } } func medialibrary(_ medialibrary: VLCMediaLibrary, didAdd artists: [VLCMLArtist]) { print("VLCMediaLibraryDelegate: Did add artists: \(artists), with count: \(artists.count)") for observer in observers { observer.value.observer?.medialibrary?(self, didAddArtist: artists) } } func medialibrary(_ medialibrary: VLCMediaLibrary, didAdd albums: [VLCMLAlbum]) { print("VLCMediaLibraryDelegate: Did add albums: \(albums), with count: \(albums.count)") for observer in observers { observer.value.observer?.medialibrary?(self, didAddAlbum: albums) } } func medialibrary(_ medialibrary: VLCMediaLibrary, didStartDiscovery entryPoint: String) { } func medialibrary(_ medialibrary: VLCMediaLibrary, didCompleteDiscovery entryPoint: String) { } func medialibrary(_ medialibrary: VLCMediaLibrary, didProgressDiscovery entryPoint: String) { } func medialibrary(_ medialibrary: VLCMediaLibrary, didUpdateParsingStatsWithPercent percent: UInt32) { } } // MARK: Future MediaDataSource extension // Todo: implement the remove // - (void)removeMediaObjectFromFolder:(NSManagedObject *)managedObject // { // NSAssert(([managedObject isKindOfClass:[MLFile class]] && ((MLFile *)managedObject).labels.count > 0), @"All media in a folder should be of type MLFile and it should be in a folder"); // // if (![managedObject isKindOfClass:[MLFile class]]) return; // // MLFile *mediaFile = (MLFile *)managedObject; // [self rearrangeFolderTrackNumbersForRemovedItem:mediaFile]; // mediaFile.labels = nil; // mediaFile.folderTrackNumber = nil; // } // // - (void)removeMediaObject:(NSManagedObject *)managedObject // { // if ([managedObject isKindOfClass:[MLAlbum class]]) { // MLAlbum *album = (MLAlbum *)managedObject; // NSSet *iterAlbumTrack = [NSSet setWithSet:album.tracks]; // // for (MLAlbumTrack *track in iterAlbumTrack) { // NSSet *iterFiles = [NSSet setWithSet:track.files]; // // for (MLFile *file in iterFiles) // [self _deleteMediaObject:file]; // } // [[MLMediaLibrary sharedMediaLibrary] removeObject: album]; // // delete all episodes from a show // } else if ([managedObject isKindOfClass:[MLShow class]]) { // MLShow *show = (MLShow *)managedObject; // NSSet *iterShowEpisodes = [NSSet setWithSet:show.episodes]; // // for (MLShowEpisode *episode in iterShowEpisodes) { // NSSet *iterFiles = [NSSet setWithSet:episode.files]; // // for (MLFile *file in iterFiles) // [self _deleteMediaObject:file]; // } // [[MLMediaLibrary sharedMediaLibrary] removeObject: show]; // // delete all files from an episode // } else if ([managedObject isKindOfClass:[MLShowEpisode class]]) { // MLShowEpisode *episode = (MLShowEpisode *)managedObject; // NSSet *iterFiles = [NSSet setWithSet:episode.files]; // // for (MLFile *file in iterFiles) // [self _deleteMediaObject:file]; // // delete all files from a track // [[MLMediaLibrary sharedMediaLibrary] removeObject: episode]; // } else if ([managedObject isKindOfClass:[MLAlbumTrack class]]) { // MLAlbumTrack *track = (MLAlbumTrack *)managedObject; // NSSet *iterFiles = [NSSet setWithSet:track.files]; // // for (MLFile *file in iterFiles) // [self _deleteMediaObject:file]; // } else if ([managedObject isKindOfClass:[MLLabel class]]) { // MLLabel *folder = (MLLabel *)managedObject; // NSSet *iterFiles = [NSSet setWithSet:folder.files]; // [folder removeFiles:folder.files]; // for (MLFile *file in iterFiles) // [self _deleteMediaObject:file]; // [[MLMediaLibrary sharedMediaLibrary] removeObject:folder]; // } // else // [self _deleteMediaObject:(MLFile *)managedObject]; // } // // - (void)_deleteMediaObject:(MLFile *)mediaObject // { // [self rearrangeFolderTrackNumbersForRemovedItem:mediaObject]; // // /* stop playback if needed */ // VLCPlaybackController *vpc = [VLCPlaybackController sharedInstance]; // VLCMedia *media = [vpc currentlyPlayingMedia]; // MLFile *currentlyPlayingFile = [MLFile fileForURL:media.url].firstObject; // if (currentlyPlayingFile && currentlyPlayingFile == mediaObject) { // [vpc stopPlayback]; // } // // NSFileManager *fileManager = [NSFileManager defaultManager]; // NSString *folderLocation = [[mediaObject.url path] stringByDeletingLastPathComponent]; // NSArray *allfiles = [fileManager contentsOfDirectoryAtPath:folderLocation error:nil]; // NSString *fileName = [mediaObject.path.lastPathComponent stringByDeletingPathExtension]; // if (!fileName) // return; // NSIndexSet *indexSet = [allfiles indexesOfObjectsPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop) { // return ([obj rangeOfString:fileName].location != NSNotFound); // }]; // NSUInteger count = indexSet.count; // NSString *additionalFilePath; // NSUInteger currentIndex = [indexSet firstIndex]; // for (unsigned int x = 0; x < count; x++) { // additionalFilePath = allfiles[currentIndex]; // if ([additionalFilePath isSupportedSubtitleFormat]) // [fileManager removeItemAtPath:[folderLocation stringByAppendingPathComponent:additionalFilePath] error:nil]; // currentIndex = [indexSet indexGreaterThanIndex:currentIndex]; // } // [fileManager removeItemAtURL:mediaObject.url error:nil]; // } // // - (void)rearrangeFolderTrackNumbersForRemovedItem:(MLFile *) mediaObject // { // MLLabel *label = [mediaObject.labels anyObject]; // NSSet *allFiles = label.files; // for (MLFile *file in allFiles) { // if (file.folderTrackNumber > mediaObject.folderTrackNumber) { // int value = [file.folderTrackNumber intValue]; // file.folderTrackNumber = [NSNumber numberWithInt:value - 1]; // } // } // } // @end