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 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) {
Log.d(TAG, "NO Scan DATA")
call.resolve(JSObject()) 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))
// //

View file

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

View file

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

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 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
}
} }
} }

View file

@ -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() {