mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-08-29 22:29:29 +02:00
Merge branch 'master' of github.com:jramer/audiobookshelf-app
This commit is contained in:
commit
c9ad0c7897
18 changed files with 123 additions and 50 deletions
|
@ -51,7 +51,7 @@ class FolderScanner(var ctx: Context) {
|
||||||
var mediaItemsUpToDate = 0
|
var mediaItemsUpToDate = 0
|
||||||
|
|
||||||
// Search for files in media item folder
|
// Search for files in media item folder
|
||||||
val foldersFound = df.search(false, DocumentFileType.FOLDER)
|
val foldersFound = df.search(true, DocumentFileType.FOLDER)
|
||||||
|
|
||||||
// Match folders found with local library items already saved in db
|
// Match folders found with local library items already saved in db
|
||||||
var existingLocalLibraryItems = DeviceManager.dbManager.getLocalLibraryItemsInFolder(localFolder.id)
|
var existingLocalLibraryItems = DeviceManager.dbManager.getLocalLibraryItemsInFolder(localFolder.id)
|
||||||
|
@ -72,13 +72,18 @@ class FolderScanner(var ctx: Context) {
|
||||||
Log.d(tag, "Iterating over Folder Found ${itemFolder.name} | ${itemFolder.getSimplePath(ctx)} | URI: ${itemFolder.uri}")
|
Log.d(tag, "Iterating over Folder Found ${itemFolder.name} | ${itemFolder.getSimplePath(ctx)} | URI: ${itemFolder.uri}")
|
||||||
val existingItem = existingLocalLibraryItems.find { emi -> emi.id == getLocalLibraryItemId(itemFolder.id) }
|
val existingItem = existingLocalLibraryItems.find { emi -> emi.id == getLocalLibraryItemId(itemFolder.id) }
|
||||||
|
|
||||||
when (scanLibraryItemFolder(itemFolder, localFolder, existingItem, forceAudioProbe)) {
|
val filesInFolder = itemFolder.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4", "application/octet-stream"))
|
||||||
|
|
||||||
|
// Do not scan folders that have no media items and not an existing item already
|
||||||
|
if (existingItem != null || filesInFolder.isNotEmpty()) {
|
||||||
|
when (scanLibraryItemFolder(itemFolder, filesInFolder, localFolder, existingItem, forceAudioProbe)) {
|
||||||
ItemScanResult.REMOVED -> mediaItemsRemoved++
|
ItemScanResult.REMOVED -> mediaItemsRemoved++
|
||||||
ItemScanResult.UPDATED -> mediaItemsUpdated++
|
ItemScanResult.UPDATED -> mediaItemsUpdated++
|
||||||
ItemScanResult.ADDED -> mediaItemsAdded++
|
ItemScanResult.ADDED -> mediaItemsAdded++
|
||||||
else -> mediaItemsUpToDate++
|
else -> mediaItemsUpToDate++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Log.d(tag, "Folder $${localFolder.name} scan Results: $mediaItemsAdded Added | $mediaItemsUpdated Updated | $mediaItemsRemoved Removed | $mediaItemsUpToDate Up-to-date")
|
Log.d(tag, "Folder $${localFolder.name} scan Results: $mediaItemsAdded Added | $mediaItemsUpdated Updated | $mediaItemsRemoved Removed | $mediaItemsUpToDate Up-to-date")
|
||||||
|
|
||||||
|
@ -91,7 +96,7 @@ class FolderScanner(var ctx: Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun scanLibraryItemFolder(itemFolder:DocumentFile, localFolder:LocalFolder, existingItem:LocalLibraryItem?, forceAudioProbe:Boolean):ItemScanResult {
|
private fun scanLibraryItemFolder(itemFolder:DocumentFile, filesInFolder:List<DocumentFile>, localFolder:LocalFolder, existingItem:LocalLibraryItem?, forceAudioProbe:Boolean):ItemScanResult {
|
||||||
val itemFolderName = itemFolder.name ?: ""
|
val itemFolderName = itemFolder.name ?: ""
|
||||||
val itemId = getLocalLibraryItemId(itemFolder.id)
|
val itemId = getLocalLibraryItemId(itemFolder.id)
|
||||||
|
|
||||||
|
@ -106,8 +111,6 @@ class FolderScanner(var ctx: Context) {
|
||||||
var coverContentUrl:String? = null
|
var coverContentUrl:String? = null
|
||||||
var coverAbsolutePath:String? = null
|
var coverAbsolutePath:String? = null
|
||||||
|
|
||||||
val filesInFolder = itemFolder.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4", "application/octet-stream"))
|
|
||||||
|
|
||||||
val existingLocalFilesRemoved = existingLocalFiles.filter { elf ->
|
val existingLocalFilesRemoved = existingLocalFiles.filter { elf ->
|
||||||
filesInFolder.find { fif -> DeviceManager.getBase64Id(fif.id) == elf.id } == null // File was not found in media item folder
|
filesInFolder.find { fif -> DeviceManager.getBase64Id(fif.id) == elf.id } == null // File was not found in media item folder
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,10 +22,11 @@ import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
class DownloadItemManager(var downloadManager:DownloadManager, private var folderScanner: FolderScanner, var mainActivity: MainActivity, private var clientEventEmitter:DownloadEventEmitter) {
|
class DownloadItemManager(var downloadManager:DownloadManager, private var folderScanner: FolderScanner, var mainActivity: MainActivity, private var clientEventEmitter:DownloadEventEmitter) {
|
||||||
val tag = "DownloadItemManager"
|
val tag = "DownloadItemManager"
|
||||||
private val maxSimultaneousDownloads = 1
|
private val maxSimultaneousDownloads = 3
|
||||||
private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
|
private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
|
||||||
|
|
||||||
enum class DownloadCheckStatus {
|
enum class DownloadCheckStatus {
|
||||||
|
@ -188,7 +189,8 @@ class DownloadItemManager(var downloadManager:DownloadManager, private var folde
|
||||||
|
|
||||||
// Rename to fix appended .mp3 on m4b/m4a files
|
// Rename to fix appended .mp3 on m4b/m4a files
|
||||||
// REF: https://github.com/anggrayudi/SimpleStorage/issues/94
|
// REF: https://github.com/anggrayudi/SimpleStorage/issues/94
|
||||||
if (resultDocFile.name?.endsWith(".m4b.mp3") == true || resultDocFile.name?.endsWith(".m4a.mp3") == true) {
|
val docNameLowerCase = resultDocFile.name?.lowercase(Locale.getDefault()) ?: ""
|
||||||
|
if (docNameLowerCase.endsWith(".m4b.mp3")|| docNameLowerCase.endsWith(".m4a.mp3")) {
|
||||||
resultDocFile.renameTo(downloadItemPart.filename)
|
resultDocFile.renameTo(downloadItemPart.filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ data class DownloadItem(
|
||||||
for (it in downloadItemParts) {
|
for (it in downloadItemParts) {
|
||||||
if (!it.completed && it.downloadId == null) {
|
if (!it.completed && it.downloadId == null) {
|
||||||
itemParts.add(it)
|
itemParts.add(it)
|
||||||
if (itemParts.size > limit) break
|
if (itemParts.size >= limit) break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,9 @@ export default {
|
||||||
bookmarks() {
|
bookmarks() {
|
||||||
if (!this.serverLibraryItemId) return []
|
if (!this.serverLibraryItemId) return []
|
||||||
return this.$store.getters['user/getUserBookmarksForItem'](this.serverLibraryItemId)
|
return this.$store.getters['user/getUserBookmarksForItem'](this.serverLibraryItemId)
|
||||||
|
},
|
||||||
|
isIos() {
|
||||||
|
return this.$platform === 'ios'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -269,9 +272,13 @@ export default {
|
||||||
this.notifyOnReady()
|
this.notifyOnReady()
|
||||||
},
|
},
|
||||||
notifyOnReady() {
|
notifyOnReady() {
|
||||||
|
// TODO: iOS opens last active playback session on app launch. Should be consistent with Android
|
||||||
|
if (!this.isIos) return
|
||||||
|
|
||||||
// If settings aren't loaded yet, native player will receive incorrect settings
|
// If settings aren't loaded yet, native player will receive incorrect settings
|
||||||
console.log('Notify on ready... settingsLoaded:', this.settingsLoaded, 'isReady:', this.isReady)
|
console.log('Notify on ready... settingsLoaded:', this.settingsLoaded, 'isReady:', this.isReady)
|
||||||
if (this.settingsLoaded && this.isReady) {
|
if (this.settingsLoaded && this.isReady && this.$store.state.isFirstAudioLoad) {
|
||||||
|
this.$store.commit('setIsFirstAudioLoad', false) // Only run this once on app launch
|
||||||
AbsAudioPlayer.onReady()
|
AbsAudioPlayer.onReady()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,6 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
clickedIt() {
|
clickedIt() {
|
||||||
if (this.isIos) return // TODO: Implement on iOS
|
|
||||||
this.$router.push('/downloading')
|
this.$router.push('/downloading')
|
||||||
},
|
},
|
||||||
onItemDownloadComplete(data) {
|
onItemDownloadComplete(data) {
|
||||||
|
|
|
@ -14,7 +14,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
// Override point for customization after application launch.
|
// Override point for customization after application launch.
|
||||||
|
|
||||||
let configuration = Realm.Configuration(
|
let configuration = Realm.Configuration(
|
||||||
schemaVersion: 6,
|
schemaVersion: 7,
|
||||||
migrationBlock: { [weak self] migration, oldSchemaVersion in
|
migrationBlock: { [weak self] migration, oldSchemaVersion in
|
||||||
if (oldSchemaVersion < 1) {
|
if (oldSchemaVersion < 1) {
|
||||||
self?.logger.log("Realm schema version was \(oldSchemaVersion)")
|
self?.logger.log("Realm schema version was \(oldSchemaVersion)")
|
||||||
|
|
|
@ -33,6 +33,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
|
||||||
handleDownloadTaskUpdate(downloadTask: downloadTask) { downloadItem, downloadItemPart in
|
handleDownloadTaskUpdate(downloadTask: downloadTask) { downloadItem, downloadItemPart in
|
||||||
let realm = try Realm()
|
let realm = try Realm()
|
||||||
try realm.write {
|
try realm.write {
|
||||||
|
downloadItemPart.bytesDownloaded = downloadItemPart.fileSize
|
||||||
downloadItemPart.progress = 100
|
downloadItemPart.progress = 100
|
||||||
downloadItemPart.completed = true
|
downloadItemPart.completed = true
|
||||||
}
|
}
|
||||||
|
@ -72,9 +73,11 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
|
||||||
handleDownloadTaskUpdate(downloadTask: downloadTask) { downloadItem, downloadItemPart in
|
handleDownloadTaskUpdate(downloadTask: downloadTask) { downloadItem, downloadItemPart in
|
||||||
// Calculate the download percentage
|
// Calculate the download percentage
|
||||||
let percentDownloaded = (Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)) * 100
|
let percentDownloaded = (Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)) * 100
|
||||||
|
|
||||||
// Only update the progress if we received accurate progress data
|
// Only update the progress if we received accurate progress data
|
||||||
if percentDownloaded >= 0.0 && percentDownloaded <= 100.0 {
|
if percentDownloaded >= 0.0 && percentDownloaded <= 100.0 {
|
||||||
try Realm().write {
|
try Realm().write {
|
||||||
|
downloadItemPart.bytesDownloaded = Double(totalBytesWritten)
|
||||||
downloadItemPart.progress = percentDownloaded
|
downloadItemPart.progress = percentDownloaded
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -109,6 +112,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
|
||||||
// Call the progress handler
|
// Call the progress handler
|
||||||
do {
|
do {
|
||||||
try progressHandler(downloadItem, part)
|
try progressHandler(downloadItem, part)
|
||||||
|
try? self.notifyListeners("onDownloadItemPartUpdate", data: part.asDictionary())
|
||||||
} catch {
|
} catch {
|
||||||
logger.error("Error while processing progress")
|
logger.error("Error while processing progress")
|
||||||
debugPrint(error)
|
debugPrint(error)
|
||||||
|
@ -158,10 +162,9 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emit status for active downloads
|
// Check for items done downloading
|
||||||
if let activeDownloads = fetchActiveDownloads() {
|
if let activeDownloads = fetchActiveDownloads() {
|
||||||
for item in activeDownloads.values {
|
for item in activeDownloads.values {
|
||||||
try? self.notifyListeners("onItemDownloadUpdate", data: item.asDictionary())
|
|
||||||
if item.isDoneDownloading() { handleDoneDownloadItem(item) }
|
if item.isDoneDownloading() { handleDoneDownloadItem(item) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -277,19 +280,22 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
|
||||||
let downloadItem = DownloadItem(libraryItem: item, episodeId: episodeId, server: Store.serverConfig!)
|
let downloadItem = DownloadItem(libraryItem: item, episodeId: episodeId, server: Store.serverConfig!)
|
||||||
var tasks = [DownloadItemPartTask]()
|
var tasks = [DownloadItemPartTask]()
|
||||||
for (i, track) in tracks.enumerated() {
|
for (i, track) in tracks.enumerated() {
|
||||||
let task = try startLibraryItemTrackDownload(item: item, position: i, track: track, episode: episode)
|
let task = try startLibraryItemTrackDownload(downloadItemId: downloadItem.id!, item: item, position: i, track: track, episode: episode)
|
||||||
downloadItem.downloadItemParts.append(task.part)
|
downloadItem.downloadItemParts.append(task.part)
|
||||||
tasks.append(task)
|
tasks.append(task)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also download the cover
|
// Also download the cover
|
||||||
if item.media?.coverPath != nil && !(item.media?.coverPath!.isEmpty ?? true) {
|
if item.media?.coverPath != nil && !(item.media?.coverPath!.isEmpty ?? true) {
|
||||||
if let task = try? startLibraryItemCoverDownload(item: item) {
|
if let task = try? startLibraryItemCoverDownload(downloadItemId: downloadItem.id!, item: item) {
|
||||||
downloadItem.downloadItemParts.append(task.part)
|
downloadItem.downloadItemParts.append(task.part)
|
||||||
tasks.append(task)
|
tasks.append(task)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Notify client of download item
|
||||||
|
try? self.notifyListeners("onDownloadItem", data: downloadItem.asDictionary())
|
||||||
|
|
||||||
// Persist in the database before status start coming in
|
// Persist in the database before status start coming in
|
||||||
try Database.shared.saveDownloadItem(downloadItem)
|
try Database.shared.saveDownloadItem(downloadItem)
|
||||||
|
|
||||||
|
@ -299,7 +305,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func startLibraryItemTrackDownload(item: LibraryItem, position: Int, track: AudioTrack, episode: PodcastEpisode?) throws -> DownloadItemPartTask {
|
private func startLibraryItemTrackDownload(downloadItemId: String, item: LibraryItem, position: Int, track: AudioTrack, episode: PodcastEpisode?) throws -> DownloadItemPartTask {
|
||||||
logger.log("TRACK \(track.contentUrl!)")
|
logger.log("TRACK \(track.contentUrl!)")
|
||||||
|
|
||||||
// If we don't name metadata, then we can't proceed
|
// If we don't name metadata, then we can't proceed
|
||||||
|
@ -312,7 +318,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
|
||||||
let localUrl = "\(itemDirectory)/\(filename)"
|
let localUrl = "\(itemDirectory)/\(filename)"
|
||||||
|
|
||||||
let task = session.downloadTask(with: serverUrl)
|
let task = session.downloadTask(with: serverUrl)
|
||||||
let part = DownloadItemPart(filename: filename, destination: localUrl, itemTitle: track.title ?? "Unknown", serverPath: Store.serverConfig!.address, audioTrack: track, episode: episode)
|
let part = DownloadItemPart(downloadItemId: downloadItemId, filename: filename, destination: localUrl, itemTitle: track.title ?? "Unknown", serverPath: Store.serverConfig!.address, audioTrack: track, episode: episode, size: track.metadata?.size ?? 0)
|
||||||
|
|
||||||
// Store the id on the task so the download item can be pulled from the database later
|
// Store the id on the task so the download item can be pulled from the database later
|
||||||
task.taskDescription = part.id
|
task.taskDescription = part.id
|
||||||
|
@ -320,13 +326,18 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
|
||||||
return DownloadItemPartTask(part: part, task: task)
|
return DownloadItemPartTask(part: part, task: task)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func startLibraryItemCoverDownload(item: LibraryItem) throws -> DownloadItemPartTask {
|
private func startLibraryItemCoverDownload(downloadItemId: String, item: LibraryItem) throws -> DownloadItemPartTask {
|
||||||
let filename = "cover.jpg"
|
let filename = "cover.jpg"
|
||||||
let serverPath = "/api/items/\(item.id)/cover"
|
let serverPath = "/api/items/\(item.id)/cover"
|
||||||
let itemDirectory = try createLibraryItemFileDirectory(item: item)
|
let itemDirectory = try createLibraryItemFileDirectory(item: item)
|
||||||
let localUrl = "\(itemDirectory)/\(filename)"
|
let localUrl = "\(itemDirectory)/\(filename)"
|
||||||
|
|
||||||
let part = DownloadItemPart(filename: filename, destination: localUrl, itemTitle: "cover", serverPath: serverPath, audioTrack: nil, episode: nil)
|
// Find library file to get cover size
|
||||||
|
let coverLibraryFile = item.libraryFiles.first(where: {
|
||||||
|
$0.metadata?.path == item.media?.coverPath
|
||||||
|
})
|
||||||
|
|
||||||
|
let part = DownloadItemPart(downloadItemId: downloadItemId, filename: filename, destination: localUrl, itemTitle: "cover", serverPath: serverPath, audioTrack: nil, episode: nil, size: coverLibraryFile?.metadata?.size ?? 0)
|
||||||
let task = session.downloadTask(with: part.downloadURL!)
|
let task = session.downloadTask(with: part.downloadURL!)
|
||||||
|
|
||||||
// Store the id on the task so the download item can be pulled from the database later
|
// Store the id on the task so the download item can be pulled from the database later
|
||||||
|
|
|
@ -9,7 +9,7 @@ import Foundation
|
||||||
import RealmSwift
|
import RealmSwift
|
||||||
|
|
||||||
class DownloadItem: Object, Codable {
|
class DownloadItem: Object, Codable {
|
||||||
@Persisted(primaryKey: true) var id: String?
|
@Persisted(primaryKey: true) var id:String?
|
||||||
@Persisted(indexed: true) var libraryItemId: String?
|
@Persisted(indexed: true) var libraryItemId: String?
|
||||||
@Persisted var episodeId: String?
|
@Persisted var episodeId: String?
|
||||||
@Persisted var userMediaProgress: MediaProgress?
|
@Persisted var userMediaProgress: MediaProgress?
|
||||||
|
|
|
@ -10,7 +10,9 @@ import RealmSwift
|
||||||
|
|
||||||
class DownloadItemPart: Object, Codable {
|
class DownloadItemPart: Object, Codable {
|
||||||
@Persisted(primaryKey: true) var id = ""
|
@Persisted(primaryKey: true) var id = ""
|
||||||
|
@Persisted var downloadItemId: String?
|
||||||
@Persisted var filename: String?
|
@Persisted var filename: String?
|
||||||
|
@Persisted var fileSize: Double = 0
|
||||||
@Persisted var itemTitle: String?
|
@Persisted var itemTitle: String?
|
||||||
@Persisted var serverPath: String?
|
@Persisted var serverPath: String?
|
||||||
@Persisted var audioTrack: AudioTrack?
|
@Persisted var audioTrack: AudioTrack?
|
||||||
|
@ -21,9 +23,10 @@ class DownloadItemPart: Object, Codable {
|
||||||
@Persisted var uri: String?
|
@Persisted var uri: String?
|
||||||
@Persisted var destinationUri: String?
|
@Persisted var destinationUri: String?
|
||||||
@Persisted var progress: Double = 0
|
@Persisted var progress: Double = 0
|
||||||
|
@Persisted var bytesDownloaded: Double = 0
|
||||||
|
|
||||||
private enum CodingKeys : String, CodingKey {
|
private enum CodingKeys : String, CodingKey {
|
||||||
case id, filename, itemTitle, completed, moved, failed, progress
|
case id, downloadItemId, filename, fileSize, itemTitle, completed, moved, failed, progress, bytesDownloaded
|
||||||
}
|
}
|
||||||
|
|
||||||
override init() {
|
override init() {
|
||||||
|
@ -33,32 +36,40 @@ class DownloadItemPart: Object, Codable {
|
||||||
required init(from decoder: Decoder) throws {
|
required init(from decoder: Decoder) throws {
|
||||||
let values = try decoder.container(keyedBy: CodingKeys.self)
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
id = try values.decode(String.self, forKey: .id)
|
id = try values.decode(String.self, forKey: .id)
|
||||||
|
downloadItemId = try? values.decode(String.self, forKey: .downloadItemId)
|
||||||
filename = try? values.decode(String.self, forKey: .filename)
|
filename = try? values.decode(String.self, forKey: .filename)
|
||||||
|
fileSize = try values.decode(Double.self, forKey: .fileSize)
|
||||||
itemTitle = try? values.decode(String.self, forKey: .itemTitle)
|
itemTitle = try? values.decode(String.self, forKey: .itemTitle)
|
||||||
completed = try values.decode(Bool.self, forKey: .completed)
|
completed = try values.decode(Bool.self, forKey: .completed)
|
||||||
moved = try values.decode(Bool.self, forKey: .moved)
|
moved = try values.decode(Bool.self, forKey: .moved)
|
||||||
failed = try values.decode(Bool.self, forKey: .failed)
|
failed = try values.decode(Bool.self, forKey: .failed)
|
||||||
progress = try values.decode(Double.self, forKey: .progress)
|
progress = try values.decode(Double.self, forKey: .progress)
|
||||||
|
bytesDownloaded = try values.decode(Double.self, forKey: .bytesDownloaded)
|
||||||
}
|
}
|
||||||
|
|
||||||
func encode(to encoder: Encoder) throws {
|
func encode(to encoder: Encoder) throws {
|
||||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
try container.encode(id, forKey: .id)
|
try container.encode(id, forKey: .id)
|
||||||
|
try container.encode(downloadItemId, forKey: .downloadItemId)
|
||||||
try container.encode(filename, forKey: .filename)
|
try container.encode(filename, forKey: .filename)
|
||||||
|
try container.encode(fileSize, forKey: .fileSize)
|
||||||
try container.encode(itemTitle, forKey: .itemTitle)
|
try container.encode(itemTitle, forKey: .itemTitle)
|
||||||
try container.encode(completed, forKey: .completed)
|
try container.encode(completed, forKey: .completed)
|
||||||
try container.encode(moved, forKey: .moved)
|
try container.encode(moved, forKey: .moved)
|
||||||
try container.encode(failed, forKey: .failed)
|
try container.encode(failed, forKey: .failed)
|
||||||
try container.encode(progress, forKey: .progress)
|
try container.encode(progress, forKey: .progress)
|
||||||
|
try container.encode(bytesDownloaded, forKey: .bytesDownloaded)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension DownloadItemPart {
|
extension DownloadItemPart {
|
||||||
convenience init(filename: String, destination: String, itemTitle: String, serverPath: String, audioTrack: AudioTrack?, episode: PodcastEpisode?) {
|
convenience init(downloadItemId: String, filename: String, destination: String, itemTitle: String, serverPath: String, audioTrack: AudioTrack?, episode: PodcastEpisode?, size: Double) {
|
||||||
self.init()
|
self.init()
|
||||||
|
|
||||||
self.id = destination.toBase64()
|
self.id = destination.toBase64()
|
||||||
|
self.downloadItemId = downloadItemId
|
||||||
self.filename = filename
|
self.filename = filename
|
||||||
|
self.fileSize = size
|
||||||
self.itemTitle = itemTitle
|
self.itemTitle = itemTitle
|
||||||
self.serverPath = serverPath
|
self.serverPath = serverPath
|
||||||
self.audioTrack = AudioTrack.detachCopy(of: audioTrack)
|
self.audioTrack = AudioTrack.detachCopy(of: audioTrack)
|
||||||
|
|
|
@ -13,9 +13,10 @@ class FileMetadata: EmbeddedObject, Codable {
|
||||||
@Persisted var ext: String = ""
|
@Persisted var ext: String = ""
|
||||||
@Persisted var path: String = ""
|
@Persisted var path: String = ""
|
||||||
@Persisted var relPath: String = ""
|
@Persisted var relPath: String = ""
|
||||||
|
@Persisted var size:Double = 0
|
||||||
|
|
||||||
private enum CodingKeys : String, CodingKey {
|
private enum CodingKeys : String, CodingKey {
|
||||||
case filename, ext, path, relPath
|
case filename, ext, path, relPath, size
|
||||||
}
|
}
|
||||||
|
|
||||||
override init() {
|
override init() {
|
||||||
|
@ -29,6 +30,7 @@ class FileMetadata: EmbeddedObject, Codable {
|
||||||
ext = try values.decode(String.self, forKey: .ext)
|
ext = try values.decode(String.self, forKey: .ext)
|
||||||
path = try values.decode(String.self, forKey: .path)
|
path = try values.decode(String.self, forKey: .path)
|
||||||
relPath = try values.decode(String.self, forKey: .relPath)
|
relPath = try values.decode(String.self, forKey: .relPath)
|
||||||
|
size = try values.decode(Double.self, forKey: .size)
|
||||||
}
|
}
|
||||||
|
|
||||||
func encode(to encoder: Encoder) throws {
|
func encode(to encoder: Encoder) throws {
|
||||||
|
@ -37,5 +39,6 @@ class FileMetadata: EmbeddedObject, Codable {
|
||||||
try container.encode(ext, forKey: .ext)
|
try container.encode(ext, forKey: .ext)
|
||||||
try container.encode(path, forKey: .path)
|
try container.encode(path, forKey: .path)
|
||||||
try container.encode(relPath, forKey: .relPath)
|
try container.encode(relPath, forKey: .relPath)
|
||||||
|
try container.encode(size, forKey: .size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,10 @@
|
||||||
<app-appbar />
|
<app-appbar />
|
||||||
<div id="content" class="overflow-hidden relative" :class="playerIsOpen ? 'playerOpen' : ''">
|
<div id="content" class="overflow-hidden relative" :class="playerIsOpen ? 'playerOpen' : ''">
|
||||||
<Nuxt />
|
<Nuxt />
|
||||||
|
|
||||||
|
<div v-if="attemptingConnection" class="absolute top-0 left-0 z-50 w-full h-full flex items-center justify-center">
|
||||||
|
<ui-loading-indicator text="Connecting to server..." />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<app-audio-player-container ref="streamContainer" />
|
<app-audio-player-container ref="streamContainer" />
|
||||||
<modals-libraries-modal />
|
<modals-libraries-modal />
|
||||||
|
|
|
@ -3,7 +3,10 @@
|
||||||
<home-bookshelf-nav-bar />
|
<home-bookshelf-nav-bar />
|
||||||
<home-bookshelf-toolbar v-show="!hideToolbar" />
|
<home-bookshelf-toolbar v-show="!hideToolbar" />
|
||||||
<div id="bookshelf-wrapper" class="main-content overflow-y-auto overflow-x-hidden relative" :class="hideToolbar ? 'no-toolbar' : ''">
|
<div id="bookshelf-wrapper" class="main-content overflow-y-auto overflow-x-hidden relative" :class="hideToolbar ? 'no-toolbar' : ''">
|
||||||
<nuxt-child />
|
<nuxt-child :loading.sync="loading" />
|
||||||
|
</div>
|
||||||
|
<div v-if="loading" class="absolute top-0 left-0 z-50 w-full h-full flex items-center justify-center">
|
||||||
|
<ui-loading-indicator text="Loading..." />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -11,7 +14,9 @@
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {
|
||||||
|
loading: false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
hideToolbar() {
|
hideToolbar() {
|
||||||
|
|
|
@ -26,20 +26,18 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="loading" class="absolute top-0 left-0 w-full h-full flex items-center justify-center">
|
|
||||||
<ui-loading-indicator text="Loading Library..." />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
|
props: {
|
||||||
|
loading: Boolean
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
shelves: [],
|
shelves: [],
|
||||||
loading: false,
|
|
||||||
isFirstNetworkConnection: true,
|
isFirstNetworkConnection: true,
|
||||||
isFirstAutoOpenPlayer: true,
|
|
||||||
lastServerFetch: 0,
|
lastServerFetch: 0,
|
||||||
lastServerFetchLibraryId: null,
|
lastServerFetchLibraryId: null,
|
||||||
lastLocalFetch: 0,
|
lastLocalFetch: 0,
|
||||||
|
@ -78,6 +76,9 @@ export default {
|
||||||
networkConnected() {
|
networkConnected() {
|
||||||
return this.$store.state.networkConnected
|
return this.$store.state.networkConnected
|
||||||
},
|
},
|
||||||
|
isIos() {
|
||||||
|
return this.$platform === 'ios'
|
||||||
|
},
|
||||||
currentLibraryName() {
|
currentLibraryName() {
|
||||||
return this.$store.getters['libraries/getCurrentLibraryName']
|
return this.$store.getters['libraries/getCurrentLibraryName']
|
||||||
},
|
},
|
||||||
|
@ -95,6 +96,14 @@ export default {
|
||||||
},
|
},
|
||||||
localMediaProgress() {
|
localMediaProgress() {
|
||||||
return this.$store.state.globals.localMediaProgress
|
return this.$store.state.globals.localMediaProgress
|
||||||
|
},
|
||||||
|
isLoading: {
|
||||||
|
get() {
|
||||||
|
return this.loading
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit('update:loading', val)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -211,7 +220,7 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loading = true
|
this.isLoading = true
|
||||||
this.shelves = []
|
this.shelves = []
|
||||||
|
|
||||||
if (this.user && this.currentLibraryId && this.networkConnected) {
|
if (this.user && this.currentLibraryId && this.networkConnected) {
|
||||||
|
@ -227,7 +236,7 @@ export default {
|
||||||
this.shelves = localCategories
|
this.shelves = localCategories
|
||||||
this.lastServerFetch = 0
|
this.lastServerFetch = 0
|
||||||
this.lastLocalFetch = Date.now()
|
this.lastLocalFetch = Date.now()
|
||||||
this.loading = false
|
this.isLoading = false
|
||||||
console.log('[categories] Local shelves set from failure', this.shelves.length, this.lastLocalFetch)
|
console.log('[categories] Local shelves set from failure', this.shelves.length, this.lastLocalFetch)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -248,7 +257,10 @@ export default {
|
||||||
return cat
|
return cat
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// TODO: iOS has its own implementation of this. Android & iOS should be consistent here.
|
||||||
|
if (!this.isIos) {
|
||||||
this.openMediaPlayerWithMostRecentListening()
|
this.openMediaPlayerWithMostRecentListening()
|
||||||
|
}
|
||||||
|
|
||||||
// Only add the local shelf with the same media type
|
// Only add the local shelf with the same media type
|
||||||
const localShelves = localCategories.filter((cat) => cat.type === this.currentLibraryMediaType && !cat.localOnly)
|
const localShelves = localCategories.filter((cat) => cat.type === this.currentLibraryMediaType && !cat.localOnly)
|
||||||
|
@ -262,13 +274,13 @@ export default {
|
||||||
console.log('[categories] Local shelves set', this.shelves.length, this.lastLocalFetch)
|
console.log('[categories] Local shelves set', this.shelves.length, this.lastLocalFetch)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loading = false
|
this.isLoading = false
|
||||||
},
|
},
|
||||||
openMediaPlayerWithMostRecentListening() {
|
openMediaPlayerWithMostRecentListening() {
|
||||||
// If we don't already have a player open
|
// If we don't already have a player open
|
||||||
// Try opening the first book from continue-listening without playing it
|
// Try opening the first book from continue-listening without playing it
|
||||||
if (this.$store.state.playerLibraryItemId || !this.isFirstAutoOpenPlayer) return
|
if (this.$store.state.playerLibraryItemId || !this.$store.state.isFirstAudioLoad) return
|
||||||
this.isFirstAutoOpenPlayer = false // Only run this once, not on every library change
|
this.$store.commit('setIsFirstAudioLoad', false) // Only run this once on app launch
|
||||||
|
|
||||||
const continueListeningShelf = this.shelves.find((cat) => cat.id === 'continue-listening')
|
const continueListeningShelf = this.shelves.find((cat) => cat.id === 'continue-listening')
|
||||||
const mostRecentEntity = continueListeningShelf?.entities?.[0]
|
const mostRecentEntity = continueListeningShelf?.entities?.[0]
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full py-6 px-2">
|
<div class="w-full h-full py-6 px-2">
|
||||||
<div v-if="localLibraryItem" class="w-full h-full" :class="orderChanged ? 'pb-20' : ''">
|
<div v-if="localLibraryItem" class="w-full h-full">
|
||||||
<div class="px-2 flex items-center mb-2">
|
<div class="px-2 flex items-center mb-2">
|
||||||
<p class="text-basefont-semibold">{{ mediaMetadata.title }}</p>
|
<p class="text-basefont-semibold">{{ mediaMetadata.title }}</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
@ -18,7 +18,7 @@
|
||||||
<div v-if="isScanning" class="w-full text-center p-4">
|
<div v-if="isScanning" class="w-full text-center p-4">
|
||||||
<p>Scanning...</p>
|
<p>Scanning...</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="w-full max-w-full media-item-container overflow-y-auto overflow-x-hidden relative">
|
<div v-else class="w-full max-w-full media-item-container overflow-y-auto overflow-x-hidden relative" :class="{ 'media-order-changed': orderChanged }">
|
||||||
<div v-if="!isPodcast" class="w-full">
|
<div v-if="!isPodcast" class="w-full">
|
||||||
<p class="text-base mb-2">Audio Tracks ({{ audioTracks.length }})</p>
|
<p class="text-base mb-2">Audio Tracks ({{ audioTracks.length }})</p>
|
||||||
|
|
||||||
|
@ -395,6 +395,18 @@ export default {
|
||||||
height: calc(100vh - 200px);
|
height: calc(100vh - 200px);
|
||||||
max-height: calc(100vh - 200px);
|
max-height: calc(100vh - 200px);
|
||||||
}
|
}
|
||||||
|
.media-item-container.media-order-changed {
|
||||||
|
height: calc(100vh - 280px);
|
||||||
|
max-height: calc(100vh - 280px);
|
||||||
|
}
|
||||||
|
.playerOpen .media-item-container {
|
||||||
|
height: calc(100vh - 300px);
|
||||||
|
max-height: calc(100vh - 300px);
|
||||||
|
}
|
||||||
|
.playerOpen .media-item-container.media-order-changed {
|
||||||
|
height: calc(100vh - 380px);
|
||||||
|
max-height: calc(100vh - 380px);
|
||||||
|
}
|
||||||
.sortable-ghost {
|
.sortable-ghost {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,14 +37,13 @@
|
||||||
<template v-for="(item, index) in mostRecentListeningSessions">
|
<template v-for="(item, index) in mostRecentListeningSessions">
|
||||||
<div :key="item.id" class="w-full py-0.5">
|
<div :key="item.id" class="w-full py-0.5">
|
||||||
<div class="flex items-center mb-1">
|
<div class="flex items-center mb-1">
|
||||||
<p class="text-smtext-white text-opacity-70 w-8">{{ index + 1 }}. </p>
|
<p class="text-sm text-white text-opacity-70 w-8 min-w-8">{{ index + 1 }}. </p>
|
||||||
<div class="w-56">
|
<div class="w-56">
|
||||||
<p class="text-smtext-white text-opacity-80 truncate">{{ item.mediaMetadata ? item.mediaMetadata.title : '' }}</p>
|
<p class="text-sm text-white text-opacity-80 truncate">{{ item.mediaMetadata ? item.mediaMetadata.title : '' }}</p>
|
||||||
<p class="text-xs text-white text-opacity-50">{{ $dateDistanceFromNow(item.updatedAt) }}</p>
|
<p class="text-xs text-white text-opacity-50">{{ $dateDistanceFromNow(item.updatedAt) }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow" />
|
<div class="w-16 min-w-16 text-right">
|
||||||
<div class="w-18 text-right">
|
<p class="text-xs font-bold">{{ $elapsedPretty(item.timeListening) }}</p>
|
||||||
<p class="text-sm font-bold">{{ $elapsedPretty(item.timeListening) }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -130,8 +130,8 @@ export const mutations = {
|
||||||
downloadItem.downloadItemParts = downloadItem.downloadItemParts.map(dip => {
|
downloadItem.downloadItemParts = downloadItem.downloadItemParts.map(dip => {
|
||||||
let newDip = dip.id == downloadItemPart.id ? downloadItemPart : dip
|
let newDip = dip.id == downloadItemPart.id ? downloadItemPart : dip
|
||||||
|
|
||||||
totalBytes += newDip.fileSize
|
totalBytes += newDip.completed ? Number(newDip.bytesDownloaded) : Number(newDip.fileSize)
|
||||||
totalBytesDownloaded += newDip.bytesDownloaded
|
totalBytesDownloaded += Number(newDip.bytesDownloaded)
|
||||||
|
|
||||||
return newDip
|
return newDip
|
||||||
})
|
})
|
||||||
|
|
|
@ -15,6 +15,7 @@ export const state = () => ({
|
||||||
networkConnectionType: null,
|
networkConnectionType: null,
|
||||||
isNetworkUnmetered: true,
|
isNetworkUnmetered: true,
|
||||||
isFirstLoad: true,
|
isFirstLoad: true,
|
||||||
|
isFirstAudioLoad: true,
|
||||||
hasStoragePermission: false,
|
hasStoragePermission: false,
|
||||||
selectedLibraryItem: null,
|
selectedLibraryItem: null,
|
||||||
showReader: false,
|
showReader: false,
|
||||||
|
@ -120,6 +121,9 @@ export const mutations = {
|
||||||
setIsFirstLoad(state, val) {
|
setIsFirstLoad(state, val) {
|
||||||
state.isFirstLoad = val
|
state.isFirstLoad = val
|
||||||
},
|
},
|
||||||
|
setIsFirstAudioLoad(state, val) {
|
||||||
|
state.isFirstAudioLoad = val
|
||||||
|
},
|
||||||
setSocketConnected(state, val) {
|
setSocketConnected(state, val) {
|
||||||
state.socketConnected = val
|
state.socketConnected = val
|
||||||
},
|
},
|
||||||
|
|
|
@ -45,7 +45,8 @@ module.exports = {
|
||||||
'4': '1rem',
|
'4': '1rem',
|
||||||
'8': '2rem',
|
'8': '2rem',
|
||||||
'10': '2.5rem',
|
'10': '2.5rem',
|
||||||
'12': '3rem'
|
'12': '3rem',
|
||||||
|
'16': '4rem'
|
||||||
},
|
},
|
||||||
minHeight: {
|
minHeight: {
|
||||||
'12': '3rem'
|
'12': '3rem'
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue