...
 
Commits (114)
......@@ -24,6 +24,7 @@ target 'VLC-iOS' do
pod 'PAPasscode', '~>1.0'
pod 'GoogleAPIClient/Drive'
pod 'MobileVLCKit', '3.1.3'
pod 'VLCMediaLibraryKit'
pod 'MediaLibraryKit-prod'
pod 'GTMAppAuth'
target 'VLC for iOSUITests' do
......
......@@ -55,6 +55,8 @@ PODS:
- upnpx (1.4.0)
- VLC-LiveSDK (5.7.0x)
- VLC-WhiteRaccoon (1.0.0)
- VLCMediaLibraryKit (0.0.4):
- MobileVLCKit
- XKKeychain (1.0.1)
- xmlrpc (2.3.4):
- "NSData+Base64 (~> 1.0.0)"
......@@ -81,6 +83,7 @@ DEPENDENCIES:
- upnpx (~> 1.4.0)
- VLC-LiveSDK (= 5.7.0x)
- VLC-WhiteRaccoon
- VLCMediaLibraryKit
- XKKeychain (~> 1.0)
SPEC REPOS:
......@@ -105,6 +108,7 @@ SPEC REPOS:
- upnpx
- VLC-LiveSDK
- VLC-WhiteRaccoon
- VLCMediaLibraryKit
- XKKeychain
- xmlrpc
......@@ -165,9 +169,10 @@ SPEC CHECKSUMS:
upnpx: c695b99229e08154d23abe5c252cb64f1600f35d
VLC-LiveSDK: c9566a9edde968f969138f84cfd40b540a109b3f
VLC-WhiteRaccoon: 1e7e59b0568959135a89d09c416d1e11a5d9a986
VLCMediaLibraryKit: 44925062bb94a50e27bf3389a2f7aa4f007f0825
XKKeychain: 852ef663c56a7194c73d3c68e8d9d4f07b121d4f
xmlrpc: 109bb21d15ed6d108b2c1ac5973a6a223a50f5f4
PODFILE CHECKSUM: 0c8231ce51e9fc3523731b5752caab4a1bef5e13
PODFILE CHECKSUM: 94325405e77c452a6048fb7a5a3c81cedf39ded7
COCOAPODS: 1.5.2
COCOAPODS: 1.5.3
/*****************************************************************************
* AlbumModel.swift
*
* Copyright © 2018 VLC authors and VideoLAN
* Copyright © 2018 Videolabs
*
* Authors: Soomin Lee <bubu@mikan.io>
*
* Refer to the COPYING file of the official project for license.
*****************************************************************************/
class AlbumModel: MLBaseModel {
typealias MLType = VLCMLAlbum
var updateView: (() -> Void)?
var files = [VLCMLAlbum]()
var medialibrary: VLCMediaLibraryManager
var indicatorName: String = NSLocalizedString("ALBUMS", comment: "")
required init(medialibrary: VLCMediaLibraryManager) {
self.medialibrary = medialibrary
medialibrary.addObserver(self)
files = medialibrary.getAlbums()
}
func append(_ item: VLCMLAlbum) {
files.append(item)
}
func delete(_ items: [VLCMLObject]) {
preconditionFailure("AlbumModel: Cannot delete album")
}
}
extension AlbumModel: MediaLibraryObserver {
func medialibrary(_ medialibrary: VLCMediaLibraryManager, didAddAlbums albums: [VLCMLAlbum]) {
print("AlbumModel: didAddAlbum: \(albums.count)")
albums.forEach({ append($0) })
updateView?()
}
}
/*****************************************************************************
* ArtistModel.swift
*
* Copyright © 2018 VLC authors and VideoLAN
* Copyright © 2018 Videolabs
*
* Authors: Soomin Lee <bubu@mikan.io>
*
* Refer to the COPYING file of the official project for license.
*****************************************************************************/
class ArtistModel: MLBaseModel {
typealias MLType = VLCMLArtist
var updateView: (() -> Void)?
var files = [VLCMLArtist]()
var medialibrary: VLCMediaLibraryManager
var indicatorName: String = NSLocalizedString("ARTISTS", comment: "")
required init(medialibrary: VLCMediaLibraryManager) {
self.medialibrary = medialibrary
medialibrary.addObserver(self)
files = medialibrary.getArtists()
}
func append(_ item: VLCMLArtist) {
files.append(item)
}
func delete(_ items: [VLCMLObject]) {
preconditionFailure("ArtistModel: Cannot delete artist")
}
}
extension ArtistModel: MediaLibraryObserver {
func medialibrary(_ medialibrary: VLCMediaLibraryManager, didAddArtists artists: [VLCMLArtist]) {
print("ArtistModel: didAddArtists: \(artists.count)")
artists.forEach({ append($0) })
updateView?()
}
}
/*****************************************************************************
* AudioModel.swift
*
* Copyright © 2018 VLC authors and VideoLAN
* Copyright © 2018 Videolabs
*
* Authors: Soomin Lee <bubu@mikan.io>
*
* Refer to the COPYING file of the official project for license.
*****************************************************************************/
class AudioModel: MLBaseModel {
typealias MLType = VLCMLMedia
var updateView: (() -> Void)?
var files = [VLCMLMedia]()
var medialibrary: VLCMediaLibraryManager
var indicatorName: String = NSLocalizedString("SONGS", comment: "")
required init(medialibrary: VLCMediaLibraryManager) {
self.medialibrary = medialibrary
medialibrary.addObserver(self)
files = medialibrary.media(ofType: .audio)
}
func append(_ item: VLCMLMedia) {
if !files.contains { $0 == item } {
files.append(item)
}
}
func delete(_ items: [VLCMLObject]) {
do {
for case let media as VLCMLMedia in items {
try FileManager.default.removeItem(atPath: media.mainFile().mrl.path)
}
medialibrary.reload()
}
catch let error as NSError {
assertionFailure("VideoModel: Delete failed: \(error.localizedDescription)")
}
}
}
// MARK: - Sort
extension AudioModel {
func sort(by criteria: VLCMLSortingCriteria) {
// FIXME: Currently if sorted by name, the files are sorted by filename but displaying title
files = medialibrary.media(ofType: .audio, sortingCriteria: criteria, desc: false)
updateView?()
}
}
// MARK: - MediaLibraryObserver
extension AudioModel: MediaLibraryObserver {
func medialibrary(_ medialibrary: VLCMediaLibraryManager, didAddAudios audios: [VLCMLMedia]) {
print("AudioModel: didAddAudio: \(audios.count)")
audios.forEach({ append($0) })
updateView?()
}
func medialibrary(_ medialibrary: VLCMediaLibraryManager, didDeleteMediaWithIds ids: [NSNumber]) {
print("AudioModel: didDeleteMedia: \(ids)")
files = files.filter() {
for id in ids where $0.identifier() == id.int64Value {
return false
}
return true
}
updateView?()
}
}
/*****************************************************************************
* GenreModel.swift
*
* Copyright © 2018 VLC authors and VideoLAN
* Copyright © 2018 Videolabs
*
* Authors: Soomin Lee <bubu@mikan.io>
*
* Refer to the COPYING file of the official project for license.
*****************************************************************************/
class GenreModel: MLBaseModel {
typealias MLType = VLCMLGenre
var updateView: (() -> Void)?
var files = [VLCMLGenre]()
var medialibrary: VLCMediaLibraryManager
var indicatorName: String = NSLocalizedString("GENRES", comment: "")
required init(medialibrary: VLCMediaLibraryManager) {
self.medialibrary = medialibrary
medialibrary.addObserver(self)
files = medialibrary.genre()
}
func append(_ item: VLCMLGenre) {
files.append(item)
}
func delete(_ items: [VLCMLObject]) {
preconditionFailure("GenreModel: Cannot delete genre")
}
}
extension GenreModel: MediaLibraryObserver {
func medialibrary(_ medialibrary: VLCMediaLibraryManager, didAddGenres genres: [VLCMLGenre]) {
print("GenreModel: didAddGenre: \(genres.count)")
genres.forEach({ append($0) })
updateView?()
}
}
/*****************************************************************************
* MediaLibraryBaseModel.swift
*
* Copyright © 2018 VLC authors and VideoLAN
* Copyright © 2018 Videolabs
*
* Authors: Soomin Lee <bubu@mikan.io>
*
* Refer to the COPYING file of the official project for license.
*****************************************************************************/
// Expose a "shadow" version without associatedType in order to use it as a type
protocol MediaLibraryBaseModel {
init(medialibrary: VLCMediaLibraryManager)
var anyfiles: [VLCMLObject] { get }
var updateView: (() -> Void)? { get set }
var indicatorName: String { get }
func append(_ item: VLCMLObject)
func delete(_ items: [VLCMLObject])
func sort(by criteria: VLCMLSortingCriteria)
}
protocol MLBaseModel: MediaLibraryBaseModel {
associatedtype MLType where MLType: VLCMLObject
init(medialibrary: VLCMediaLibraryManager)
var files: [MLType] { get set }
var medialibrary: VLCMediaLibraryManager { get }
var updateView: (() -> Void)? { get set }
var indicatorName: String { get }
func append(_ item: MLType)
// FIXME: Ideally items should be MLType but Swift isn't happy so it will always fail
func delete(_ items: [VLCMLObject])
func sort(by criteria: VLCMLSortingCriteria)
}
extension MLBaseModel {
var anyfiles: [VLCMLObject] {
return files
}
func append(_ item: VLCMLObject) {
fatalError()
}
func delete(_ items: [VLCMLObject]) {
fatalError()
}
func sort(by criteria: VLCMLSortingCriteria) {
fatalError()
}
}
/*****************************************************************************
* PlaylistModel.swift
*
* Copyright © 2018 VLC authors and VideoLAN
* Copyright © 2018 Videolabs
*
* Authors: Soomin Lee <bubu@mikan.io>
*
* Refer to the COPYING file of the official project for license.
*****************************************************************************/
class PlaylistModel: MLBaseModel {
typealias MLType = VLCMLPlaylist
var updateView: (() -> Void)?
var files = [VLCMLPlaylist]()
var medialibrary: VLCMediaLibraryManager
var indicatorName: String = NSLocalizedString("PLAYLISTS", comment: "")
required init(medialibrary: VLCMediaLibraryManager) {
self.medialibrary = medialibrary
medialibrary.addObserver(self)
files = medialibrary.getPlaylists()
}
func append(_ item: VLCMLPlaylist) {
for file in files {
if file.identifier() == item.identifier() {
return
}
}
files.append(item)
}
func delete(_ items: [VLCMLObject]) {
for playlist in items where playlist is VLCMLPlaylist {
if !(medialibrary.deletePlaylist(with: playlist.identifier())) {
assertionFailure("PlaylistModel: Failed to delete playlist: \(playlist.identifier())")
}
}
}
// Creates a VLCMLPlaylist appending it and updates linked view
func create(name: String) {
append(medialibrary.createPlaylist(with: name))
updateView?()
}
}
extension PlaylistModel: MediaLibraryObserver {
func medialibrary(_ medialibrary: VLCMediaLibraryManager, didAddPlaylists playlists: [VLCMLPlaylist]) {
print("PlaylistModel: didAddPlaylists: \(playlists.count)")
playlists.forEach({ append($0) })
updateView?()
}
func medialibrary(_ medialibrary: VLCMediaLibraryManager, didDeletePlaylistsWithIds playlistsIds: [NSNumber]) {
print("PlaylistModel: didDeletePlaylists: \(playlistsIds)")
files = files.filter() {
for id in playlistsIds where $0.identifier() == id.int64Value {
return false
}
return true
}
updateView?()
}
}
/*****************************************************************************
* ShowEpisodeModel.swift
*
* Copyright © 2018 VLC authors and VideoLAN
* Copyright © 2018 Videolabs
*
* Authors: Soomin Lee <bubu@mikan.io>
*
* Refer to the COPYING file of the official project for license.
*****************************************************************************/
class ShowEpisodeModel: MLBaseModel {
typealias MLType = VLCMLMedia
var updateView: (() -> Void)?
var files = [VLCMLMedia]()
var medialibrary: VLCMediaLibraryManager
var indicatorName: String = NSLocalizedString("EPISODES", comment: "")
required init(medialibrary: VLCMediaLibraryManager) {
self.medialibrary = medialibrary
medialibrary.addObserver(self)
}
func append(_ item: VLCMLMedia) {
files.append(item)
}
func delete(_ items: [VLCMLObject]) {
preconditionFailure("ShowEpisodeModel: Cannot delete showEpisode")
}
}
extension ShowEpisodeModel: MediaLibraryObserver {
func medialibrary(_ medialibrary: VLCMediaLibraryManager, didAddShowEpisodes showEpisodes: [VLCMLMedia]) {
print("ShowEpisode: didAddShowEpisode: \(showEpisodes.count)")
showEpisodes.forEach({ append($0) })
updateView?()
}
}
/*****************************************************************************
* VideoModel.swift
*
* Copyright © 2018 VLC authors and VideoLAN
* Copyright © 2018 Videolabs
*
* Authors: Soomin Lee <bubu@mikan.io>
*
* Refer to the COPYING file of the official project for license.
*****************************************************************************/
class VideoModel: MLBaseModel {
typealias MLType = VLCMLMedia
var updateView: (() -> Void)?
var files = [VLCMLMedia]()
var medialibrary: VLCMediaLibraryManager
var indicatorName: String = NSLocalizedString("MOVIES", comment: "")
required init(medialibrary: VLCMediaLibraryManager) {
self.medialibrary = medialibrary
medialibrary.addObserver(self)
files = medialibrary.media(ofType: .video)
medialibrary.requestThumbnail(for: files)
}
func append(_ item: VLCMLMedia) {
if !files.contains { $0 == item } {
files.append(item)
}
}
func delete(_ items: [VLCMLObject]) {
do {
for case let media as VLCMLMedia in items {
try FileManager.default.removeItem(atPath: media.mainFile().mrl.path)
}
medialibrary.reload()
}
catch let error as NSError {
assertionFailure("VideoModel: Delete failed: \(error.localizedDescription)")
}
}
}
// MARK: - Sort
extension VideoModel {
func sort(by criteria: VLCMLSortingCriteria) {
files = medialibrary.media(ofType: .video, sortingCriteria: criteria, desc: false)
updateView?()
}
}
// MARK: - MediaLibraryObserver
extension VideoModel: MediaLibraryObserver {
func medialibrary(_ medialibrary: VLCMediaLibraryManager, didAddVideos videos: [VLCMLMedia]) {
print("VideoModel: didAddVideo: \(videos.count)")
videos.forEach({ append($0) })
updateView?()
}
func medialibrary(_ medialibrary: VLCMediaLibraryManager, didDeleteMediaWithIds ids: [NSNumber]) {
print("VideoModel: didDeleteMedia: \(ids)")
files = files.filter() {
for id in ids where $0.identifier() == id.int64Value {
return false
}
return true
}
updateView?()
}
}
// MARK: MediaLibraryObserver - Thumbnail
extension VideoModel {
func medialibrary(_ medialibrary: VLCMediaLibraryManager, thumbnailReady media: VLCMLMedia) {
for (index, file) in files.enumerated() {
if file == media {
files[index] = media
break
}
}
updateView?()
}
}
extension VLCMLMedia {
static func == (lhs: VLCMLMedia, rhs: VLCMLMedia) -> Bool {
return lhs.identifier() == rhs.identifier()
}
}
extension VLCMLMedia {
@objc func formatDuration() -> String {
return String(format: "%@", VLCTime(int: Int32(duration())))
}
@objc func formatSize() -> String {
return ByteCountFormatter.string(fromByteCount: Int64(mainFile().size()),
countStyle: .file)
}
}
/*****************************************************************************
* 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.
*****************************************************************************/
extension Notification.Name {
static let VLCNewFileAddedNotification = Notification.Name("NewFileAddedNotification")
}
// For objc
extension NSNotification {
@objc static let VLCNewFileAddedNotification = Notification.Name.VLCNewFileAddedNotification
}
@objc protocol MediaLibraryObserver: class {
// Video
@objc optional func medialibrary(_ medialibrary: VLCMediaLibraryManager,
didModifyVideo video: [VLCMLMedia])
@objc optional func medialibrary(_ medialibrary: VLCMediaLibraryManager,
didDeleteMediaWithIds ids: [NSNumber])
@objc optional func medialibrary(_ medialibrary: VLCMediaLibraryManager,
didAddVideos videos: [VLCMLMedia])
@objc optional func medialibrary(_ medialibrary: VLCMediaLibraryManager,
didAddShowEpisodes showEpisodes: [VLCMLMedia])
@objc optional func medialibrary(_ medialibrary: VLCMediaLibraryManager,
thumbnailReady media: VLCMLMedia)
// Audio
@objc optional func medialibrary(_ medialibrary: VLCMediaLibraryManager,
didAddAudios audios: [VLCMLMedia])
@objc optional func medialibrary(_ medialibrary: VLCMediaLibraryManager,
didAddArtists artists: [VLCMLArtist])
@objc optional func medialibrary(_ medialibrary: VLCMediaLibraryManager,
didDeleteArtistsWithIds artistsIds: [NSNumber])
@objc optional func medialibrary(_ medialibrary: VLCMediaLibraryManager,
didAddAlbums albums: [VLCMLAlbum])
@objc optional func medialibrary(_ medialibrary: VLCMediaLibraryManager,
didDeleteAlbumsWithIds albumsIds: [NSNumber])
@objc optional func medialibrary(_ medialibrary: VLCMediaLibraryManager,
didAddAlbumTracks albumTracks: [VLCMLMedia])
@objc optional func medialibrary(_ medialibrary: VLCMediaLibraryManager,
didAddGenres genres: [VLCMLGenre])
// Playlist
@objc optional func medialibrary(_ medialibrary: VLCMediaLibraryManager,
didAddPlaylists playlists: [VLCMLPlaylist])
@objc optional func medialibrary(_ medialibrary: VLCMediaLibraryManager,
didDeletePlaylistsWithIds playlistsIds: [NSNumber])
}
class VLCMediaLibraryManager: NSObject {
private static let databaseName: String = "medialibrary.db"
// 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
medialibrary.deviceListerDelegate = 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 libraryPath = NSSearchPathForDirectoriesInDomains(.libraryDirectory, .userDomainMask, true).first else {
preconditionFailure("VLCMediaLibraryManager: Unable to init medialibrary.")
}
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)")
}
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)
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
@objc 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?
}
}
extension VLCMediaLibraryManager {
func addObserver(_ observer: MediaLibraryObserver) {
let identifier = ObjectIdentifier(observer)
observers[identifier] = Observer(observer: observer)
}
func removeObserver(_ observer: MediaLibraryObserver) {
let identifier = ObjectIdentifier(observer)
observers.removeValue(forKey: identifier)
}
}
// MARK: MediaLibrary - Audio methods
extension VLCMediaLibraryManager {
func getArtists(sortingCriteria sort: VLCMLSortingCriteria = .artist, desc: Bool = false) -> [VLCMLArtist] {
return medialib.artists(with: sort, desc: desc, all: true)
}
func getAlbums(sortingCriteria sort: VLCMLSortingCriteria = .album, desc: Bool = false) -> [VLCMLAlbum] {
return medialib.albums(with: sort, desc: desc)
}
}
// MARK: MediaLibrary - Video methods
extension VLCMediaLibraryManager {
func requestThumbnail(for media: [VLCMLMedia]) {
media.forEach() {
if !medialib.requestThumbnail(for: $0) {
assertionFailure("VLCMediaLibraryManager: Failed to generate thumbnail for: \($0.identifier())")
}
}
}
}
// MARK: MediaLibrary - Playlist methods
extension VLCMediaLibraryManager {
func createPlaylist(with name: String) -> VLCMLPlaylist {
return medialib.createPlaylist(withName: name)
}
func deletePlaylist(with identifier: VLCMLIdentifier) -> Bool {
return medialib.deletePlaylist(withIdentifier: identifier)
}
func getPlaylists(sortingCriteria sort: VLCMLSortingCriteria = .default, desc: Bool = false) -> [VLCMLPlaylist] {
return medialib.playlists(with: sort, desc: desc)
}
}
// MARK: - VLCMediaLibraryDelegate - Media
extension VLCMediaLibraryManager: VLCMediaLibraryDelegate {
func medialibrary(_ medialibrary: VLCMediaLibrary, didAddMedia media: [VLCMLMedia]) {
print("VLCMediaLibraryDelegate: Did add media: \(media), with count: \(media.count)")
print("VLCMediaLibraryDelegate: video count: \(medialibrary.videoFiles(with: .default, desc: false).count)")
print("VLCMediaLibraryDelegate: audio count: \(medialibrary.audioFiles(with: .default, desc: false).count)")
let videos = media.filter {( $0.type() == .video )}
let audio = media.filter {( $0.type() == .audio )}
// thumbnails only for videos
requestThumbnail(for: videos)
for observer in observers {
observer.value.observer?.medialibrary?(self, didAddVideos: videos)
observer.value.observer?.medialibrary?(self, didAddAudios: audio)
}
}
func medialibrary(_ medialibrary: VLCMediaLibrary, didModifyMedia media: [VLCMLMedia]) {
print("VLCMediaLibraryDelegate: Did modift media: \(media), with count: \(media.count)")
let showEpisodes = media.filter {( $0.subtype() == .showEpisode )}
let albumTrack = media.filter {( $0.subtype() == .albumTrack )}
for observer in observers {
observer.value.observer?.medialibrary?(self, didAddShowEpisodes: showEpisodes)
observer.value.observer?.medialibrary?(self, didAddAlbumTracks: albumTrack)
}
}
func medialibrary(_ medialibrary: VLCMediaLibrary, didDeleteMediaWithIds mediaIds: [NSNumber]) {
print("VLCMediaLibraryDelegate: Did delete media: \(mediaIds)")
for observer in observers {
observer.value.observer?.medialibrary?(self, didDeleteMediaWithIds: mediaIds)
}
}
func medialibrary(_ medialibrary: VLCMediaLibrary, thumbnailReadyFor media: VLCMLMedia, withSuccess success: Bool) {
print("VLCMediaLibraryDelete: Thumbnail ready?: \(success) for: \(media)")
for observer in observers {
observer.value.observer?.medialibrary?(self, thumbnailReady: media)
}
}
}
// MARK: - VLCMediaLibraryDelegate - Artists
extension VLCMediaLibraryManager {
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, didAddArtists: artists)
}
}
func medialibrary(_ medialibrary: VLCMediaLibrary, didDeleteArtistsWithIds artistsIds: [NSNumber]) {
for observer in observers {
observer.value.observer?.medialibrary?(self, didDeleteArtistsWithIds: artistsIds)
}
}
}
// MARK: - VLCMediaLibraryDelegate - Albums
extension VLCMediaLibraryManager {
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, didAddAlbums: albums)
}
}
func medialibrary(_ medialibrary: VLCMediaLibrary, didDeleteAlbumsWithIds albumsIds: [NSNumber]) {
for observer in observers {
observer.value.observer?.medialibrary?(self, didDeleteAlbumsWithIds: albumsIds)
}
}
}
// MARK: - VLCMediaLibraryDelegate - Playlists
extension VLCMediaLibraryManager {
func medialibrary(_ medialibrary: VLCMediaLibrary, didAdd playlists: [VLCMLPlaylist]) {
print("VLCMediaLibraryDelegate: Did add playlists: \(playlists), with count: \(playlists.count)")
for observer in observers {
observer.value.observer?.medialibrary?(self, didAddPlaylists: playlists)
}
}
func medialibrary(_ medialibrary: VLCMediaLibrary, didDeletePlaylistsWithIds playlistsIds: [NSNumber]) {
print("VLCMediaLibraryDelegate: Did delete playlists: \(playlistsIds), with count: \(playlistsIds.count)")
for observer in observers {
observer.value.observer?.medialibrary?(self, didDeletePlaylistsWithIds: playlistsIds)
}
}
}
// MARK: - VLCMediaLibraryDelegate - Discovery
extension VLCMediaLibraryManager {
func medialibrary(_ medialibrary: VLCMediaLibrary, didStartDiscovery entryPoint: String) {
print("VLCMediaLibraryDelegate: Did start discovery")
}
func medialibrary(_ medialibrary: VLCMediaLibrary, didCompleteDiscovery entryPoint: String) {
print("VLCMediaLibraryDelegate: Did complete discovery")
}
func medialibrary(_ medialibrary: VLCMediaLibrary, didProgressDiscovery entryPoint: String) {
print("VLCMediaLibraryDelegate: Did progress discovery")
}
func medialibrary(_ medialibrary: VLCMediaLibrary, didUpdateParsingStatsWithPercent percent: UInt32) {
print("VLCMediaLibraryDelegate: Did update parsing with percent: \(percent)")
}
}
// MARK: VLCMLDeviceListerDelegate
extension VLCMediaLibraryManager: VLCMLDeviceListerDelegate {
func medialibrary(_ medialibrary: VLCMediaLibrary, devicePluggedWithUUID uuid: String, withMountPoint mountPoint: String) -> Bool {
print("onDevicePlugged: \(uuid), mountPoint: \(mountPoint)")
return false
}
func medialibrary(_ medialibrary: VLCMediaLibrary, deviceUnPluggedWithUUID uuid: String) {
print("onDeviceUnplugged: \(uuid)")
}
func medialibrary(_ medialibrary: VLCMediaLibrary, isDeviceKnownWithUUID uuid: String) -> Bool {
print("is device known: \(uuid)")
return false
}
}
......@@ -12,7 +12,7 @@
@objc(VLCService)
class Services: NSObject {
@objc let mediaDataSource = VLCMediaDataSource()
@objc let medialibraryManager = VLCMediaLibraryManager()
@objc let rendererDiscovererManager = VLCRendererDiscovererManager(presentingViewController: nil)
}
......@@ -27,6 +27,10 @@ class Services: NSObject {
self.playerController = VLCPlayerDisplayController(services: services)
super.init()
setupPlayerController()
// Init the HTTP Server and clean its cache
// FIXME: VLCHTTPUploaderController should perhaps be a service?
VLCHTTPUploaderController.sharedInstance().cleanCache()
}
private func setupPlayerController() {
......
......@@ -10,50 +10,82 @@
* Refer to the COPYING file of the official project for license.
*****************************************************************************/
class VLCMovieCategoryViewController: VLCMediaCategoryViewController<MLFile> {
class VLCMovieCategoryViewController: VLCMediaCategoryViewController {
init(_ services: Services) {
super.init(services: services, category: VLCMediaSubcategories.movies)
let model = VideoModel(medialibrary: services.medialibraryManager)
super.init(services: services, category: model)
category.updateView = { [weak self] in
self?.reloadData()
}
}
}
class VLCShowEpisodeCategoryViewController: VLCMediaCategoryViewController<MLShowEpisode> {
class VLCShowEpisodeCategoryViewController: VLCMediaCategoryViewController {
init(_ services: Services) {
super.init(services: services, category: VLCMediaSubcategories.episodes)
let model = ShowEpisodeModel(medialibrary: services.medialibraryManager)
super.init(services: services, category: model)
category.updateView = { [weak self] in
self?.reloadData()
}
}
}
class VLCVideoPlaylistCategoryViewController: VLCMediaCategoryViewController<MLLabel> {
class VLCVideoPlaylistCategoryViewController: VLCMediaCategoryViewController {
init(_ services: Services) {
super.init(services: services, category: VLCMediaSubcategories.videoPlaylists)
let model = PlaylistModel(medialibrary: services.medialibraryManager)
super.init(services: services, category: model)
category.updateView = { [weak self] in
self?.reloadData()
}
}
}
class VLCTrackCategoryViewController: VLCMediaCategoryViewController<MLFile> {
class VLCTrackCategoryViewController: VLCMediaCategoryViewController {
init(_ services: Services) {
super.init(services: services, category: VLCMediaSubcategories.tracks)
let model = AudioModel(medialibrary: services.medialibraryManager)
super.init(services: services, category: model)
category.updateView = { [weak self] in
self?.reloadData()
}
}
}
class VLCGenreCategoryViewController: VLCMediaCategoryViewController<String> {
class VLCGenreCategoryViewController: VLCMediaCategoryViewController {
init(_ services: Services) {
super.init(services: services, category: VLCMediaSubcategories.genres)
let model = GenreModel(medialibrary: services.medialibraryManager)
super.init(services: services, category: model)
category.updateView = { [weak self] in
self?.reloadData()
}
}
}
class VLCArtistCategoryViewController: VLCMediaCategoryViewController<String> {
class VLCArtistCategoryViewController: VLCMediaCategoryViewController {
init(_ services: Services) {
super.init(services: services, category: VLCMediaSubcategories.artists)
let model = ArtistModel(medialibrary: services.medialibraryManager)
super.init(services: services, category: model)
category.updateView = { [weak self] in
self?.reloadData()
}
}
}
class VLCAlbumCategoryViewController: VLCMediaCategoryViewController<MLAlbum> {
class VLCAlbumCategoryViewController: VLCMediaCategoryViewController {
init(_ services: Services) {
super.init(services: services, category: VLCMediaSubcategories.albums)
let model = AlbumModel(medialibrary: services.medialibraryManager)
super.init(services: services, category: model)
category.updateView = { [weak self] in
self?.reloadData()
}
}
}
class VLCAudioPlaylistCategoryViewController: VLCMediaCategoryViewController<MLLabel> {
class VLCAudioPlaylistCategoryViewController: VLCMediaCategoryViewController {
init(_ services: Services) {
super.init(services: services, category: VLCMediaSubcategories.audioPlaylists)
let model = VideoModel(medialibrary: services.medialibraryManager)
super.init(services: services, category: model)
category.updateView = { [weak self] in
self?.reloadData()
}
}
}
......@@ -13,18 +13,18 @@
import Foundation
class VLCMediaCategoryViewController<T>: UICollectionViewController, UICollectionViewDelegateFlowLayout, UISearchResultsUpdating, UISearchControllerDelegate, IndicatorInfoProvider {
class VLCMediaCategoryViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout, UISearchResultsUpdating, UISearchControllerDelegate, IndicatorInfoProvider {
let cellPadding: CGFloat = 5.0
private var services: Services
private var searchController: UISearchController?
private let searchDataSource = VLCLibrarySearchDisplayDataSource()
var category: VLCMediaSubcategoryModel<T>
var category: MediaLibraryBaseModel
private lazy var editController = VLCEditController(collectionView: self.collectionView!, category: self.category)
@available(iOS 11.0, *)
lazy var dragAndDropManager: VLCDragAndDropManager = { () -> VLCDragAndDropManager<T> in
VLCDragAndDropManager<T>(subcategory: category)
}()
// @available(iOS 11.0, *)
// lazy var dragAndDropManager: VLCDragAndDropManager = { () -> VLCDragAndDropManager<T> in
// VLCDragAndDropManager<T>(subcategory: VLCMediaSubcategories<>)
// }()
lazy var emptyView: VLCEmptyLibraryView = {
let name = String(describing: VLCEmptyLibraryView.self)
......@@ -33,18 +33,23 @@ class VLCMediaCategoryViewController<T>: UICollectionViewController, UICollectio
return emptyView
}()
let editCollectionViewLayout: UICollectionViewFlowLayout = {
let editCollectionViewLayout = UICollectionViewFlowLayout()
editCollectionViewLayout.minimumLineSpacing = 1
editCollectionViewLayout.minimumInteritemSpacing = 0
return editCollectionViewLayout
}()
@available(*, unavailable)
init() {
fatalError()
}
init(services: Services, category: VLCMediaSubcategoryModel<T>) {
init(services: Services, category: MediaLibraryBaseModel) {
self.services = services
self.category = category
super.init(collectionViewLayout: UICollectionViewFlowLayout())
NotificationCenter.default.addObserver(self, selector: #selector(themeDidChange), name: .VLCThemeDidChangeNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(reloadData), name: category.changeNotificationName, object: nil)
}
override var preferredStatusBarStyle: UIStatusBarStyle {
......@@ -52,8 +57,11 @@ class VLCMediaCategoryViewController<T>: UICollectionViewController, UICollectio
}
@objc func reloadData() {
collectionView?.reloadData()
updateUIForContent()
DispatchQueue.main.async {
[weak self] in
self?.collectionView?.reloadData()
self?.updateUIForContent()
}
}
@available(*, unavailable)
......@@ -86,11 +94,12 @@ class VLCMediaCategoryViewController<T>: UICollectionViewController, UICollectio
func setupCollectionView() {
let playlistnib = UINib(nibName: "VLCPlaylistCollectionViewCell", bundle: nil)
collectionView?.register(playlistnib, forCellWithReuseIdentifier: VLCPlaylistCollectionViewCell.cellIdentifier())
collectionView?.register(VLCMediaViewEditCell.self, forCellWithReuseIdentifier: VLCMediaViewEditCell.identifier)
collectionView?.backgroundColor = PresentationTheme.current.colors.background
collectionView?.alwaysBounceVertical = true
if #available(iOS 11.0, *) {
collectionView?.dragDelegate = dragAndDropManager
collectionView?.dropDelegate = dragAndDropManager
// collectionView?.dragDelegate = dragAndDropManager
// collectionView?.dropDelegate = dragAndDropManager
}
}
......@@ -134,10 +143,32 @@ class VLCMediaCategoryViewController<T>: UICollectionViewController, UICollectio
collectionView?.collectionViewLayout.invalidateLayout()
}
// MARK: - Edit
override func setEditing(_ editing: Bool, animated: Bool) {
super.setEditing(editing, animated: animated)
// might have an issue if the old datasource was search
// Most of the edit logic is handled inside editController
collectionView?.dataSource = editing ? editController : self
collectionView?.delegate = editing ? editController : self
editController.toolbarNeedsUpdate(editing: editing)
let layoutToBe = editing ? editCollectionViewLayout : UICollectionViewFlowLayout()
collectionView?.setCollectionViewLayout(layoutToBe, animated: false, completion: {
[weak self] finished in
guard finished else {
assertionFailure("VLCMediaSubcategoryViewController: Edit layout transition failed.")
return
}
self?.reloadData()
})
}
// MARK: - Search
func updateSearchResults(for searchController: UISearchController) {
searchDataSource.shouldReloadTable(forSearch: searchController.searchBar.text, searchableFiles: category.files)
searchDataSource.shouldReloadTable(forSearch: searchController.searchBar.text, searchableFiles: category.anyfiles)
collectionView?.reloadData()
}
......@@ -150,19 +181,23 @@ class VLCMediaCategoryViewController<T>: UICollectionViewController, UICollectio
}
func indicatorInfo(for pagerTabStripController: PagerTabStripViewController) -> IndicatorInfo {
return IndicatorInfo(title:category.indicatorInfoName)
return IndicatorInfo(title:category.indicatorName)
}
// MARK: - UICollectionViewDataSource
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return category.files.count
return category.anyfiles.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if let playlistCell = collectionView.dequeueReusableCell(withReuseIdentifier: VLCPlaylistCollectionViewCell.cellIdentifier(), for: indexPath) as? VLCPlaylistCollectionViewCell {
if let mediaObject = category.files[indexPath.row] as? NSManagedObject {
if let mediaObject = category.anyfiles[indexPath.row] as? NSManagedObject {
playlistCell.mediaObject = mediaObject
}
if let media = category.anyfiles[indexPath.row] as? VLCMLMedia {
playlistCell.media = (media.mainFile() != nil) ? media : nil
}
return playlistCell
}
return UICollectionViewCell()
......@@ -170,8 +205,10 @@ class VLCMediaCategoryViewController<T>: UICollectionViewController, UICollectio
// MARK: - UICollectionViewDelegate
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if let mediaObject = category.files[indexPath.row] as? NSManagedObject {
if let mediaObject = category.anyfiles[indexPath.row] as? NSManagedObject {
play(mediaObject: mediaObject)
} else if let media = category.anyfiles[indexPath.row] as? VLCMLMedia {
play(media: media)
}
}
......@@ -203,10 +240,40 @@ class VLCMediaCategoryViewController<T>: UICollectionViewController, UICollectio
}
}
// MARK: - Sort
extension VLCMediaCategoryViewController {
// FIXME: Need to add a button for ascending/descending result
func sortByFileName() {
// The issue is that for audio we show title which is quite confusing if we use filename
category.sort(by: .alpha)
}
func sortByDate() {
category.sort(by: .insertionDate)
}
func sortBySize() {
category.sort(by: .fileSize)
}
}
// MARK: - Player
extension VLCMediaCategoryViewController {
func play(mediaObject: NSManagedObject) {
VLCPlaybackController.sharedInstance().playMediaLibraryObject(mediaObject)
}
func play(media: VLCMLMedia) {
VLCPlaybackController.sharedInstance().play(media)
}
}
// MARK: - MediaLibraryModelView
extension VLCMediaCategoryViewController {
func dataChanged() {
reloadData()
}
}
/*****************************************************************************
* VLCMediaSubcategory.swift
* VLC for iOS
*****************************************************************************
* Copyright (c) 2017 VideoLAN. All rights reserved.
* $Id$
*
* Authors: Carola Nitz <caro # videolan.org>
*
* Refer to the COPYING file of the official project for license.
*****************************************************************************/
import Foundation
extension Notification.Name {
static let VLCMoviesDidChangeNotification = Notification.Name("MoviesDidChangeNotfication")
static let VLCEpisodesDidChangeNotification = Notification.Name("EpisodesDidChangeNotfication")
static let VLCArtistsDidChangeNotification = Notification.Name("ArtistsDidChangeNotfication")
static let VLCAlbumsDidChangeNotification = Notification.Name("AlbumsDidChangeNotfication")
static let VLCTracksDidChangeNotification = Notification.Name("TracksDidChangeNotfication")
static let VLCGenresDidChangeNotification = Notification.Name("GenresDidChangeNotfication")
static let VLCAudioPlaylistsDidChangeNotification = Notification.Name("AudioPlaylistsDidChangeNotfication")
static let VLCVideoPlaylistsDidChangeNotification = Notification.Name("VideoPlaylistsDidChangeNotfication")
static let VLCVideosDidChangeNotification = Notification.Name("VideosDidChangeNotfication")
}
enum VLCDataUnit {
case file(MLFile)
case episode(MLShowEpisode)
case album(MLAlbum)
case label(MLLabel)
}
class VLCMediaSubcategoryModel<T>: NSObject {
var files: [T]
var indicatorInfoName: String
var changeNotificationName: Notification.Name
var includesFunc: (VLCDataUnit) -> Bool
var appendFunc: (VLCDataUnit) -> Void
var indicatorInfo: IndicatorInfo {
return IndicatorInfo(title: indicatorInfoName)
}
init(files: [T],
indicatorInfoName: String,
notificationName: Notification.Name,
includesFunc: @escaping (VLCDataUnit) -> Bool,
appendFunc: @escaping (VLCDataUnit) -> Void) {
self.files = files
self.indicatorInfoName = indicatorInfoName
self.changeNotificationName = notificationName
self.includesFunc = includesFunc
self.appendFunc = appendFunc
}
}
struct VLCMediaSubcategories {
static var movies = VLCMediaSubcategoryModel<MLFile>(
files: {
(MLFile.allFiles() as! [MLFile]).filter {
($0 as MLFile).isKind(ofType: kMLFileTypeMovie) ||
($0 as MLFile).isKind(ofType: kMLFileTypeTVShowEpisode) ||
($0 as MLFile).isKind(ofType: kMLFileTypeClip)
}
}(),
indicatorInfoName: NSLocalizedString("MOVIES", comment: ""),
notificationName: .VLCMoviesDidChangeNotification,
includesFunc: { (dataUnit: VLCDataUnit) in
if case .file(let f) = dataUnit {
return f.isMovie()
}
return false
},
appendFunc: { (dataUnit: VLCDataUnit) in
})
static var episodes = VLCMediaSubcategoryModel<MLShowEpisode>(
files: MLShowEpisode.allEpisodes() as! [MLShowEpisode],
indicatorInfoName: NSLocalizedString("EPISODES", comment: ""),
notificationName: .VLCEpisodesDidChangeNotification,
includesFunc: { (dataUnit: VLCDataUnit) in
if case .episode(let f) = dataUnit {
return true
}
return false
},
appendFunc: { (dataUnit: VLCDataUnit) in
})
static var artists = VLCMediaSubcategoryModel<String>(
files: {
let tracksWithArtist = (MLAlbumTrack.allTracks() as! [MLAlbumTrack]).filter { $0.artist != nil && $0.artist != "" }
return tracksWithArtist.map { $0.artist } as! [String]
}(),
indicatorInfoName: NSLocalizedString("ARTISTS", comment: ""),
notificationName: .VLCArtistsDidChangeNotification,
includesFunc: { (dataUnit: VLCDataUnit) in
if case .file(let f) = dataUnit {
return f.artist != nil
}
return false
},
appendFunc: { (dataUnit: VLCDataUnit) in
})
static var albums = VLCMediaSubcategoryModel<MLAlbum>(
files: MLAlbum.allAlbums() as! [MLAlbum],
indicatorInfoName: NSLocalizedString("ALBUMS", comment: ""),
notificationName: .VLCAlbumsDidChangeNotification,
includesFunc: { (dataUnit: VLCDataUnit) in
if case .album(let f) = dataUnit {
return true
}
return false
},
appendFunc: { (dataUnit: VLCDataUnit) in
})
static var tracks = VLCMediaSubcategoryModel<MLFile>(
files: (MLFile.allFiles() as! [MLFile]).filter { $0.isSupportedAudioFile()},
indicatorInfoName: NSLocalizedString("SONGS", comment: ""),
notificationName: .VLCTracksDidChangeNotification,
includesFunc: { (dataUnit: VLCDataUnit) in
if case .file(let f) = dataUnit {
return f.isSupportedAudioFile()
}
return false
},
appendFunc: { (dataUnit: VLCDataUnit) in
})
static var genres = VLCMediaSubcategoryModel<String>(
files: {
let albumtracks = MLAlbumTrack.allTracks() as! [MLAlbumTrack]
let tracksWithArtist = albumtracks.filter { $0.genre != nil && $0.genre != "" }
return tracksWithArtist.map { $0.genre }
}(),
indicatorInfoName: NSLocalizedString("GENRES", comment: ""),
notificationName: .VLCGenresDidChangeNotification ,
includesFunc: { (dataUnit: VLCDataUnit) in
if case .file(let f) = dataUnit {
return f.genre != nil
}
return false
},
appendFunc: { (dataUnit: VLCDataUnit) in
})
static var audioPlaylists = VLCMediaSubcategoryModel<MLLabel>(
files: {
let labels = MLLabel.allLabels() as! [MLLabel]
let audioPlaylist = labels.filter {
let audioFiles = $0.files.filter {
if let file = $0 as? MLFile {
return file.isSupportedAudioFile()
}
return false
}
return !audioFiles.isEmpty
}
return audioPlaylist
}(),
indicatorInfoName: NSLocalizedString("AUDIO_PLAYLISTS", comment: ""),
notificationName: .VLCAudioPlaylistsDidChangeNotification ,
includesFunc: { (dataUnit: VLCDataUnit) in
if case .label(let l) = dataUnit {
let audioFiles = l.files.filter {
if let file = $0 as? MLFile {
return file.isSupportedAudioFile()
} else {
return false
}
}
return !audioFiles.isEmpty
}
return false
},
appendFunc: { (dataUnit: VLCDataUnit) in
})
static var videoPlaylists = VLCMediaSubcategoryModel<MLLabel>(
files: {
let labels = MLLabel.allLabels() as! [MLLabel]
let 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
}
return audioPlaylist
}(),
indicatorInfoName: NSLocalizedString("VIDEO_PLAYLISTS", comment: ""),
notificationName: .VLCVideoPlaylistsDidChangeNotification ,
includesFunc: { (dataUnit: VLCDataUnit) in
if case .label(let l) = dataUnit {
let videoFiles = l.files.filter {
if let file = $0 as? MLFile {
return file.isShowEpisode() || file.isMovie() || file.isClip()
} else {
return false
}
}
return !videoFiles.isEmpty
}
return false
},
appendFunc: { (dataUnit: VLCDataUnit) in
})
}
......@@ -15,6 +15,7 @@ import UIKit
class VLCMediaViewController: VLCPagingViewController<VLCLabelCell> {
var services: Services
private var rendererButton: UIButton
private let fixedSpaceWidth: CGFloat = 21
init(services: Services) {
self.services = services
......@@ -37,18 +38,39 @@ class VLCMediaViewController: VLCPagingViewController<VLCLabelCell> {
if #available(iOS 11.0, *) {
navigationController?.navigationBar.prefersLargeTitles = false