mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-08-28 22:08:47 +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
|
||||
|
||||
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))
|
||||
//
|
||||
|
|
|
@ -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?
|
||||
)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue