Add new folder scanner for media items with ffprober

This commit is contained in:
advplyr 2022-03-30 19:41:04 -05:00
parent 4fc70cd3dd
commit a8de03b82d
6 changed files with 113 additions and 46 deletions

View file

@ -1,20 +1,17 @@
package com.audiobookshelf.app
import android.Manifest
import android.content.pm.PackageManager
import android.database.Cursor
import android.net.Uri
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.app.ActivityCompat
import androidx.documentfile.provider.DocumentFile
import com.anggrayudi.storage.SimpleStorage
import com.anggrayudi.storage.callback.FolderPickerCallback
import com.anggrayudi.storage.callback.StorageAccessCallback
import com.anggrayudi.storage.file.*
import com.audiobookshelf.app.device.FolderScanner
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.getcapacitor.*
import com.getcapacitor.annotation.CapacitorPlugin
@ -159,12 +156,19 @@ class StorageManager : Plugin() {
@PluginMethod
fun searchFolder(call: PluginCall) {
var folderUrl = call.data.getString("folderUrl", "").toString()
var mediaType = call.data.getString("mediaType", "book").toString()
Log.d(TAG, "Searching folder $folderUrl")
var folderScanner = FolderScanner(context)
var data = folderScanner.scanForAudiobooks(folderUrl)
Log.d(TAG, "Scan DATA $data")
var folderScanResult = folderScanner.scanForMediaItems(folderUrl, mediaType)
if (folderScanResult == null) {
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))
//

View file

@ -150,5 +150,6 @@ data class AudioTrack(
var title:String,
var contentUrl:String,
var mimeType:String,
var isLocal:Boolean
var isLocal:Boolean,
var audioProbeResult:AudioProbeResult?
)

View file

@ -1,6 +1,7 @@
package com.audiobookshelf.app.data
import android.net.Uri
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.getcapacitor.JSObject
data class ServerConfig(
@ -17,8 +18,24 @@ data class DeviceData(
var lastServerConfigId:String?
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class LocalMediaItem(
val name: String,
val simplePath: String,
val audioTracks:MutableList<AudioTrack>
var name: String,
var contentUrl:String,
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
)

View file

@ -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>,
)

View file

@ -5,39 +5,35 @@ import android.net.Uri
import android.util.Log
import androidx.documentfile.provider.DocumentFile
import com.anggrayudi.storage.file.*
import com.arthenica.ffmpegkit.FFmpegKitConfig
import com.arthenica.ffmpegkit.FFprobeKit
import com.arthenica.ffmpegkit.FFprobeSession
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.audiobookshelf.app.data.*
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
class FolderScanner(var ctx: Context) {
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))
if (df == null) {
Log.e(tag, "Folder Doc File Invalid $folderUrl")
return mutableListOf()
return null
}
var mediaFolders = mutableListOf<LocalMediaItem>()
var foldersFound = df.search(false, DocumentFileType.FOLDER)
var mediaItems = mutableListOf<LocalMediaItem>()
foldersFound.forEach {
Log.d(tag, "Iterating over Folder Found ${it.name} | ${it.getSimplePath(ctx)} | URI: ${it.uri}")
var folderName = it.name ?: ""
var mediaFiles = mutableListOf<LocalMediaItem>()
var audioTracks = mutableListOf<AudioTrack>()
var localFiles = mutableListOf<LocalFile>()
var index = 1
var startOffset = 0.0
var coverPath:String? = null
var filesInFolder = it.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*"))
filesInFolder.forEach { it2 ->
@ -46,12 +42,16 @@ class FolderScanner(var ctx: Context) {
var isAudio = mimeType.startsWith("audio")
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) {
var absolutePath = it2.getAbsolutePath(ctx)
Log.d(tag, "Audio File Path $absolutePath")
Log.d(tag, "Scanning Audio File Path ${localFile.absolutePath}")
// 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
Log.d(tag, "AFTER FFPROBE STRING $sessionData")
@ -59,19 +59,29 @@ class FolderScanner(var ctx: Context) {
val audioProbeResult = mapper.readValue<AudioProbeResult>(sessionData)
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)
startOffset += audioProbeResult.duration
} 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
}
}
}

View file

@ -56,6 +56,18 @@
<ui-btn small @click="changeDownloadFolderClick">Change Folder</ui-btn>
<ui-btn small color="error" @click="resetFolder">Reset</ui-btn>
</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-else-if="!mediaScanResults" class="text-center my-8">No Files Found</p>
<div v-else>
@ -83,6 +95,7 @@
</template>
<script>
import { Capacitor } from '@capacitor/core'
import { Dialog } from '@capacitor/dialog'
import AudioDownloader from '@/plugins/audio-downloader'
import StorageManager from '@/plugins/storage-manager'
@ -93,7 +106,8 @@ export default {
downloadingProgress: {},
totalSize: 0,
showingDownloads: true,
isScanning: false
isScanning: false,
localMediaItems: []
}
},
computed: {
@ -161,23 +175,35 @@ export default {
async searchFolder() {
this.isScanning = true
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) {
console.log('Search results folders length', searchResults.folders.length)
searchResults.folders = searchResults.folders.map((sr) => {
if (sr.files) {
sr.files = JSON.parse(sr.files)
if (response && response.localMediaItems) {
this.localMediaItems = response.localMediaItems.map((mi) => {
if (mi.coverPath) {
mi.coverPathSrc = Capacitor.convertFileSrc(mi.coverPath)
}
return sr
return mi
})
this.$store.commit('downloads/setMediaScanResults', searchResults)
console.log('Set Local Media Items', this.localMediaItems.length)
} 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
},
async resetFolder() {