mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-08-29 06:18:51 +02:00
Add new folder scanner for media items with ffprober
This commit is contained in:
parent
4fc70cd3dd
commit
a8de03b82d
6 changed files with 113 additions and 46 deletions
|
@ -1,20 +1,17 @@
|
||||||
package com.audiobookshelf.app
|
package com.audiobookshelf.app
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.app.ActivityCompat
|
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
|
||||||
import com.anggrayudi.storage.SimpleStorage
|
import com.anggrayudi.storage.SimpleStorage
|
||||||
import com.anggrayudi.storage.callback.FolderPickerCallback
|
import com.anggrayudi.storage.callback.FolderPickerCallback
|
||||||
import com.anggrayudi.storage.callback.StorageAccessCallback
|
import com.anggrayudi.storage.callback.StorageAccessCallback
|
||||||
import com.anggrayudi.storage.file.*
|
import com.anggrayudi.storage.file.*
|
||||||
import com.audiobookshelf.app.device.FolderScanner
|
import com.audiobookshelf.app.device.FolderScanner
|
||||||
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
import com.getcapacitor.*
|
import com.getcapacitor.*
|
||||||
import com.getcapacitor.annotation.CapacitorPlugin
|
import com.getcapacitor.annotation.CapacitorPlugin
|
||||||
|
|
||||||
|
@ -159,12 +156,19 @@ class StorageManager : Plugin() {
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun searchFolder(call: PluginCall) {
|
fun searchFolder(call: PluginCall) {
|
||||||
var folderUrl = call.data.getString("folderUrl", "").toString()
|
var folderUrl = call.data.getString("folderUrl", "").toString()
|
||||||
|
var mediaType = call.data.getString("mediaType", "book").toString()
|
||||||
Log.d(TAG, "Searching folder $folderUrl")
|
Log.d(TAG, "Searching folder $folderUrl")
|
||||||
|
|
||||||
var folderScanner = FolderScanner(context)
|
var folderScanner = FolderScanner(context)
|
||||||
var data = folderScanner.scanForAudiobooks(folderUrl)
|
var folderScanResult = folderScanner.scanForMediaItems(folderUrl, mediaType)
|
||||||
Log.d(TAG, "Scan DATA $data")
|
if (folderScanResult == null) {
|
||||||
call.resolve(JSObject())
|
Log.d(TAG, "NO Scan DATA")
|
||||||
|
call.resolve(JSObject())
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "Scan DATA ${jacksonObjectMapper().writeValueAsString(folderScanResult)}")
|
||||||
|
call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(folderScanResult)))
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// var df: DocumentFile? = DocumentFileCompat.fromUri(context, Uri.parse(folderUrl))
|
// var df: DocumentFile? = DocumentFileCompat.fromUri(context, Uri.parse(folderUrl))
|
||||||
//
|
//
|
||||||
|
|
|
@ -150,5 +150,6 @@ data class AudioTrack(
|
||||||
var title:String,
|
var title:String,
|
||||||
var contentUrl:String,
|
var contentUrl:String,
|
||||||
var mimeType:String,
|
var mimeType:String,
|
||||||
var isLocal:Boolean
|
var isLocal:Boolean,
|
||||||
|
var audioProbeResult:AudioProbeResult?
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package com.audiobookshelf.app.data
|
package com.audiobookshelf.app.data
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||||
import com.getcapacitor.JSObject
|
import com.getcapacitor.JSObject
|
||||||
|
|
||||||
data class ServerConfig(
|
data class ServerConfig(
|
||||||
|
@ -17,8 +18,24 @@ data class DeviceData(
|
||||||
var lastServerConfigId:String?
|
var lastServerConfigId:String?
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class LocalMediaItem(
|
data class LocalMediaItem(
|
||||||
val name: String,
|
var name: String,
|
||||||
val simplePath: String,
|
var contentUrl:String,
|
||||||
val audioTracks:MutableList<AudioTrack>
|
var simplePath: String,
|
||||||
|
var absolutePath:String,
|
||||||
|
var audioTracks:MutableList<AudioTrack>,
|
||||||
|
var localFiles:MutableList<LocalFile>,
|
||||||
|
var coverPath:String?
|
||||||
|
)
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
data class LocalFile(
|
||||||
|
var id:String,
|
||||||
|
var filename:String?,
|
||||||
|
var contentUrl:String,
|
||||||
|
var absolutePath:String,
|
||||||
|
var simplePath:String,
|
||||||
|
var mimeType:String?,
|
||||||
|
var size:Long
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
package com.audiobookshelf.app.data
|
||||||
|
|
||||||
|
data class FolderScanResult(
|
||||||
|
val name:String?,
|
||||||
|
val absolutePath:String,
|
||||||
|
val mediaType:String,
|
||||||
|
val contentUrl:String,
|
||||||
|
val localMediaItems:MutableList<LocalMediaItem>,
|
||||||
|
)
|
|
@ -5,39 +5,35 @@ import android.net.Uri
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import com.anggrayudi.storage.file.*
|
import com.anggrayudi.storage.file.*
|
||||||
import com.arthenica.ffmpegkit.FFmpegKitConfig
|
|
||||||
import com.arthenica.ffmpegkit.FFprobeKit
|
import com.arthenica.ffmpegkit.FFprobeKit
|
||||||
import com.arthenica.ffmpegkit.FFprobeSession
|
import com.audiobookshelf.app.data.*
|
||||||
import com.arthenica.ffmpegkit.Level
|
|
||||||
import com.audiobookshelf.app.data.AudioProbeResult
|
|
||||||
import com.audiobookshelf.app.data.AudioTrack
|
|
||||||
import com.audiobookshelf.app.data.LocalMediaItem
|
|
||||||
import com.audiobookshelf.app.data.PlaybackSession
|
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
import com.fasterxml.jackson.module.kotlin.readValue
|
import com.fasterxml.jackson.module.kotlin.readValue
|
||||||
|
|
||||||
|
|
||||||
class FolderScanner(var ctx: Context) {
|
class FolderScanner(var ctx: Context) {
|
||||||
private val tag = "FolderScanner"
|
private val tag = "FolderScanner"
|
||||||
|
|
||||||
fun scanForAudiobooks(folderUrl: String):MutableList<LocalMediaItem> {
|
fun scanForMediaItems(folderUrl: String, mediaType:String):FolderScanResult? {
|
||||||
var df: DocumentFile? = DocumentFileCompat.fromUri(ctx, Uri.parse(folderUrl))
|
var df: DocumentFile? = DocumentFileCompat.fromUri(ctx, Uri.parse(folderUrl))
|
||||||
|
|
||||||
if (df == null) {
|
if (df == null) {
|
||||||
Log.e(tag, "Folder Doc File Invalid $folderUrl")
|
Log.e(tag, "Folder Doc File Invalid $folderUrl")
|
||||||
return mutableListOf()
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
var mediaFolders = mutableListOf<LocalMediaItem>()
|
|
||||||
var foldersFound = df.search(false, DocumentFileType.FOLDER)
|
var foldersFound = df.search(false, DocumentFileType.FOLDER)
|
||||||
|
|
||||||
|
var mediaItems = mutableListOf<LocalMediaItem>()
|
||||||
|
|
||||||
foldersFound.forEach {
|
foldersFound.forEach {
|
||||||
Log.d(tag, "Iterating over Folder Found ${it.name} | ${it.getSimplePath(ctx)} | URI: ${it.uri}")
|
Log.d(tag, "Iterating over Folder Found ${it.name} | ${it.getSimplePath(ctx)} | URI: ${it.uri}")
|
||||||
var folderName = it.name ?: ""
|
var folderName = it.name ?: ""
|
||||||
var mediaFiles = mutableListOf<LocalMediaItem>()
|
|
||||||
|
|
||||||
var audioTracks = mutableListOf<AudioTrack>()
|
var audioTracks = mutableListOf<AudioTrack>()
|
||||||
|
var localFiles = mutableListOf<LocalFile>()
|
||||||
var index = 1
|
var index = 1
|
||||||
|
var startOffset = 0.0
|
||||||
|
var coverPath:String? = null
|
||||||
|
|
||||||
var filesInFolder = it.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*"))
|
var filesInFolder = it.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*"))
|
||||||
filesInFolder.forEach { it2 ->
|
filesInFolder.forEach { it2 ->
|
||||||
|
@ -46,12 +42,16 @@ class FolderScanner(var ctx: Context) {
|
||||||
var isAudio = mimeType.startsWith("audio")
|
var isAudio = mimeType.startsWith("audio")
|
||||||
Log.d(tag, "Found $mimeType file $filename in folder $folderName")
|
Log.d(tag, "Found $mimeType file $filename in folder $folderName")
|
||||||
|
|
||||||
|
var localFile = LocalFile(it2.id,it2.name,it2.uri.toString(),it2.getAbsolutePath(ctx),it2.getSimplePath(ctx),it2.mimeType,it2.length())
|
||||||
|
localFiles.add(localFile)
|
||||||
|
|
||||||
|
Log.d(tag, "File attributes Id:${it2.id}|ContentUrl:${localFile.contentUrl}|isDownloadsDocument:${it2.isDownloadsDocument}")
|
||||||
|
|
||||||
if (isAudio) {
|
if (isAudio) {
|
||||||
var absolutePath = it2.getAbsolutePath(ctx)
|
Log.d(tag, "Scanning Audio File Path ${localFile.absolutePath}")
|
||||||
Log.d(tag, "Audio File Path $absolutePath")
|
|
||||||
|
|
||||||
// TODO: Make asynchronous
|
// TODO: Make asynchronous
|
||||||
var session = FFprobeKit.execute("-i \"$absolutePath\" -print_format json -show_format -show_streams -select_streams a -show_chapters -loglevel quiet")
|
var session = FFprobeKit.execute("-i \"${localFile.absolutePath}\" -print_format json -show_format -show_streams -select_streams a -show_chapters -loglevel quiet")
|
||||||
var sessionData = session.output
|
var sessionData = session.output
|
||||||
Log.d(tag, "AFTER FFPROBE STRING $sessionData")
|
Log.d(tag, "AFTER FFPROBE STRING $sessionData")
|
||||||
|
|
||||||
|
@ -59,19 +59,29 @@ class FolderScanner(var ctx: Context) {
|
||||||
val audioProbeResult = mapper.readValue<AudioProbeResult>(sessionData)
|
val audioProbeResult = mapper.readValue<AudioProbeResult>(sessionData)
|
||||||
Log.d(tag, "Probe Result DATA ${audioProbeResult.duration} | ${audioProbeResult.size} | ${audioProbeResult.title} | ${audioProbeResult.artist}")
|
Log.d(tag, "Probe Result DATA ${audioProbeResult.duration} | ${audioProbeResult.size} | ${audioProbeResult.title} | ${audioProbeResult.artist}")
|
||||||
|
|
||||||
var track = AudioTrack(index, 0.0, 0.0, filename, absolutePath, mimeType, true)
|
var track = AudioTrack(index, startOffset, audioProbeResult.duration, filename, localFile.contentUrl, mimeType, true, audioProbeResult)
|
||||||
audioTracks.add(track)
|
audioTracks.add(track)
|
||||||
|
startOffset += audioProbeResult.duration
|
||||||
} else {
|
} else {
|
||||||
Log.d(tag, "Found non audio file $filename")
|
// First image file use as cover path
|
||||||
|
if (coverPath == null) {
|
||||||
|
coverPath = localFile.absolutePath
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// var imageFile = StorageManager.MediaFile(it2.uri, filename, it2.getSimplePath(context), it2.length(), mimeType, isAudio)
|
|
||||||
// mediaFiles.add(imageFile)
|
|
||||||
}
|
}
|
||||||
if (mediaFiles.size > 0) {
|
if (audioTracks.size > 0) {
|
||||||
|
Log.d(tag, "Found local media item named $folderName with ${audioTracks.size} tracks")
|
||||||
|
var localMediaItem = LocalMediaItem(folderName, it.uri.toString(), it.getSimplePath(ctx), it.getAbsolutePath(ctx),audioTracks,localFiles,coverPath)
|
||||||
|
mediaItems.add(localMediaItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return mediaFolders
|
return if (mediaItems.size > 0) {
|
||||||
|
Log.d(tag, "Found ${mediaItems.size} Media Items")
|
||||||
|
FolderScanResult(df.name, df.getAbsolutePath(ctx), mediaType, df.uri.toString(), mediaItems)
|
||||||
|
} else {
|
||||||
|
Log.d(tag, "No Media Items Found")
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,6 +56,18 @@
|
||||||
<ui-btn small @click="changeDownloadFolderClick">Change Folder</ui-btn>
|
<ui-btn small @click="changeDownloadFolderClick">Change Folder</ui-btn>
|
||||||
<ui-btn small color="error" @click="resetFolder">Reset</ui-btn>
|
<ui-btn small color="error" @click="resetFolder">Reset</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Temp testing new folder scan results -->
|
||||||
|
<div v-for="mediaItem in localMediaItems" :key="mediaItem.contentUrl" class="flex py-2">
|
||||||
|
<div class="w-12 h-12 bg-primary">
|
||||||
|
<img v-if="mediaItem.coverPathSrc" :src="mediaItem.coverPathSrc" class="w-full h-full object-contain" />
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow px-2">
|
||||||
|
<p>{{ mediaItem.name }}</p>
|
||||||
|
<p>{{ mediaItem.audioTracks.length }} Tracks</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p v-if="isScanning" class="text-center my-8">Scanning Folder..</p>
|
<p v-if="isScanning" class="text-center my-8">Scanning Folder..</p>
|
||||||
<p v-else-if="!mediaScanResults" class="text-center my-8">No Files Found</p>
|
<p v-else-if="!mediaScanResults" class="text-center my-8">No Files Found</p>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
|
@ -83,6 +95,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { Capacitor } from '@capacitor/core'
|
||||||
import { Dialog } from '@capacitor/dialog'
|
import { Dialog } from '@capacitor/dialog'
|
||||||
import AudioDownloader from '@/plugins/audio-downloader'
|
import AudioDownloader from '@/plugins/audio-downloader'
|
||||||
import StorageManager from '@/plugins/storage-manager'
|
import StorageManager from '@/plugins/storage-manager'
|
||||||
|
@ -93,7 +106,8 @@ export default {
|
||||||
downloadingProgress: {},
|
downloadingProgress: {},
|
||||||
totalSize: 0,
|
totalSize: 0,
|
||||||
showingDownloads: true,
|
showingDownloads: true,
|
||||||
isScanning: false
|
isScanning: false,
|
||||||
|
localMediaItems: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -161,23 +175,35 @@ export default {
|
||||||
async searchFolder() {
|
async searchFolder() {
|
||||||
this.isScanning = true
|
this.isScanning = true
|
||||||
var response = await StorageManager.searchFolder({ folderUrl: this.downloadFolderUri })
|
var response = await StorageManager.searchFolder({ folderUrl: this.downloadFolderUri })
|
||||||
var searchResults = response
|
|
||||||
searchResults.folders = JSON.parse(searchResults.folders)
|
|
||||||
searchResults.files = JSON.parse(searchResults.files)
|
|
||||||
|
|
||||||
if (searchResults.folders.length) {
|
if (response && response.localMediaItems) {
|
||||||
console.log('Search results folders length', searchResults.folders.length)
|
this.localMediaItems = response.localMediaItems.map((mi) => {
|
||||||
|
if (mi.coverPath) {
|
||||||
searchResults.folders = searchResults.folders.map((sr) => {
|
mi.coverPathSrc = Capacitor.convertFileSrc(mi.coverPath)
|
||||||
if (sr.files) {
|
|
||||||
sr.files = JSON.parse(sr.files)
|
|
||||||
}
|
}
|
||||||
return sr
|
return mi
|
||||||
})
|
})
|
||||||
this.$store.commit('downloads/setMediaScanResults', searchResults)
|
console.log('Set Local Media Items', this.localMediaItems.length)
|
||||||
} else {
|
} else {
|
||||||
this.$toast.warning('No audio or image files found')
|
console.log('No Local media items found')
|
||||||
}
|
}
|
||||||
|
// var searchResults = response
|
||||||
|
// searchResults.folders = JSON.parse(searchResults.folders)
|
||||||
|
// searchResults.files = JSON.parse(searchResults.files)
|
||||||
|
|
||||||
|
// if (searchResults.folders.length) {
|
||||||
|
// console.log('Search results folders length', searchResults.folders.length)
|
||||||
|
|
||||||
|
// searchResults.folders = searchResults.folders.map((sr) => {
|
||||||
|
// if (sr.files) {
|
||||||
|
// sr.files = JSON.parse(sr.files)
|
||||||
|
// }
|
||||||
|
// return sr
|
||||||
|
// })
|
||||||
|
// this.$store.commit('downloads/setMediaScanResults', searchResults)
|
||||||
|
// } else {
|
||||||
|
// this.$toast.warning('No audio or image files found')
|
||||||
|
// }
|
||||||
this.isScanning = false
|
this.isScanning = false
|
||||||
},
|
},
|
||||||
async resetFolder() {
|
async resetFolder() {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue