Compare commits

...
Author SHA1 Message Date
advplyr 75627ea40f iOS version bump 0.9.81 & pod update 2025-04-26 10:03:57 -05:00
advplyr 0fa04d7d9a Version bump v0.9.81-beta 2025-04-25 16:56:34 -05:00
advplyrandGitHub 1e76ebe075 Merge pull request #1556 from Zibbp/master
fix(InternalDownloadManager): add accept-encoding identity to requests
2025-04-25 16:25:07 -05:00
advplyrandGitHub 965da2fee3 Merge pull request #1547 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2025-04-25 16:24:51 -05:00
Zibbp b03f59ace3 fix(InternalDownloadManager): set accept-encoding header on just the download task 2025-04-25 15:51:46 -05:00
SunSpringandHosted Weblate 79c7244b24 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (349 of 349 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2025-04-25 00:29:31 +02:00
PimandHosted Weblate 0843be3f4e Translated using Weblate (Dutch)
Currently translated at 97.9% (342 of 349 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nl/
2025-04-25 00:29:31 +02:00
Kabika82andHosted Weblate 6911f368e5 Translated using Weblate (Hungarian)
Currently translated at 100.0% (349 of 349 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hu/
2025-04-25 00:29:31 +02:00
cebo29andHosted Weblate 42167ea738 Translated using Weblate (German)
Currently translated at 100.0% (349 of 349 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-04-25 00:29:31 +02:00
thehijackerandHosted Weblate 1c57d3506a Translated using Weblate (Slovenian)
Currently translated at 100.0% (349 of 349 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sl/
2025-04-25 00:29:31 +02:00
Максим ГорпинічandHosted Weblate 8adc6fae4c Translated using Weblate (Ukrainian)
Currently translated at 100.0% (349 of 349 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2025-04-25 00:29:31 +02:00
NickSkierandHosted Weblate e9746577ba Translated using Weblate (Russian)
Currently translated at 100.0% (349 of 349 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ru/
2025-04-25 00:29:31 +02:00
max grakovandHosted Weblate 9f68730622 Translated using Weblate (Russian)
Currently translated at 100.0% (349 of 349 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ru/
2025-04-25 00:29:31 +02:00
biuklijaandHosted Weblate f019b67b0c Translated using Weblate (Croatian)
Currently translated at 99.7% (348 of 349 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2025-04-25 00:29:31 +02:00
advplyrandGitHub 0f650c0572 Merge pull request #1555 from advplyr/sleep_timer_chime_android
Add:Android sleep timer setting to play a chime when almost finished
2025-04-24 17:29:25 -05:00
Zibbp dfc77ea0d0 fix(InternalDownloadManager): add accept-encoding identity to requests 2025-04-24 17:04:08 -05:00
advplyr 67f514524f Remove unused imports and release log 2025-04-24 17:03:14 -05:00
advplyr d97c6a0872 Add:Android sleep timer setting to play a chime when almost finished #600 2025-04-24 16:42:55 -05:00
advplyr e7ad62760f Fix android cover image not showing in notification for downloaded media #1493, upgrade glide 2025-04-23 17:01:52 -05:00
advplyr fd34ea8124 Show change library dropdown in app bar when socket is not connected #1494 2025-04-23 15:46:19 -05:00
advplyr 5db94bf5b8 Fix series sequence text color on book cards in light mode #1549 2025-04-22 16:50:21 -05:00
advplyr 796d6d79d4 Add logs for android auto 2025-04-21 17:34:30 -05:00
advplyr 03aafafe1c Fix bug with AbsLogger not initialized 2025-04-21 17:11:23 -05:00
advplyr 3f303abc19 iOS version bump 0.9.80 2025-04-20 17:17:18 -05:00
advplyr 85d6958025 Version bump v0.9.80-beta 2025-04-20 16:54:49 -05:00
advplyrandGitHub 669bd7827b Merge pull request #1545 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2025-04-20 16:17:36 -05:00
petr-prikrylandHosted Weblate 2b48f0c6a9 Translated using Weblate (Czech)
Currently translated at 100.0% (345 of 345 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2025-04-20 21:11:10 +00:00
ABS translatorandHosted Weblate 2c44d38906 Translated using Weblate (Arabic)
Currently translated at 52.1% (180 of 345 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ar/
2025-04-20 21:11:10 +00:00
Максим ГорпинічandHosted Weblate 750726ff6a Translated using Weblate (Ukrainian)
Currently translated at 100.0% (345 of 345 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2025-04-20 21:11:09 +00:00
Jakob ZollandHosted Weblate dafab492fe Translated using Weblate (German)
Currently translated at 100.0% (345 of 345 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-04-20 21:11:08 +00:00
advplyr 6419c8dc3a Update deploy-apk workflow for java 21 2025-04-20 16:10:54 -05:00
advplyrandGitHub 3bb5ce5924 Merge pull request #1546 from advplyr/abslogger
Logs page with AbsLogger plugin
2025-04-20 16:09:14 -05:00
advplyr 882c2749ab Add AbsLogger stub to iOS, update iOS plugins for capacitor 7 2025-04-20 16:06:52 -05:00
advplyr 26b0fae0fb Setup onLog event, add app version & platform to logs and share filename 2025-04-20 14:33:48 -05:00
advplyr fe921fd5b1 Update AbsLogger to have a tag with logs 2025-04-20 12:36:27 -05:00
advplyr 88e1877742 More AbsLogs and clean logs older than 48 hours on init 2025-04-20 12:18:10 -05:00
advplyr 74758c7762 Add clear logs, use more menu dialog 2025-04-20 10:32:52 -05:00
advplyr 2000534e37 Update logs to mask server address, add share txt file button 2025-04-20 10:06:52 -05:00
advplyr 390388fe83 Add AbsLogger plugin, persist logs to db, logs page for android 2025-04-19 17:26:32 -05:00
advplyrandGitHub b9e3ccd0c1 Merge pull request #1543 from advplyr/capacitor_v7
Update to capacitor v7
2025-04-17 17:57:22 -05:00
55 changed files with 877 additions and 244 deletions
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
uses: actions/setup-java@v2
with:
distribution: 'temurin'
java-version: 17
java-version: 21
- name: install dependencies
run: npm ci
+2 -2
View File
@@ -36,8 +36,8 @@ android {
applicationId "com.audiobookshelf.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 110
versionName "0.9.79-beta"
versionCode 112
versionName "0.9.81-beta"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
manifestPlaceholders = [
"appAuthRedirectScheme": "com.audiobookshelf.app"
@@ -18,6 +18,7 @@ import com.audiobookshelf.app.plugins.AbsAudioPlayer
import com.audiobookshelf.app.plugins.AbsDatabase
import com.audiobookshelf.app.plugins.AbsDownloader
import com.audiobookshelf.app.plugins.AbsFileSystem
import com.audiobookshelf.app.plugins.AbsLogger
import com.getcapacitor.BridgeActivity
@@ -57,6 +58,7 @@ class MainActivity : BridgeActivity() {
registerPlugin(AbsDownloader::class.java)
registerPlugin(AbsFileSystem::class.java)
registerPlugin(AbsDatabase::class.java)
registerPlugin(AbsLogger::class.java)
super.onCreate(savedInstanceState)
Log.d(tag, "onCreate")
@@ -135,6 +135,7 @@ data class DeviceSettings(
var sleepTimerLength: Long, // Time in milliseconds
var disableSleepTimerFadeOut: Boolean,
var disableSleepTimerResetFeedback: Boolean,
var enableSleepTimerAlmostDoneChime: Boolean,
var languageCode: String,
var downloadUsingCellular: DownloadUsingCellularSetting,
var streamingUsingCellular: StreamingUsingCellularSetting,
@@ -163,6 +164,7 @@ data class DeviceSettings(
autoSleepTimerAutoRewindTime = 300000L, // 5 minutes
disableSleepTimerFadeOut = false,
disableSleepTimerResetFeedback = false,
enableSleepTimerAlmostDoneChime = false,
languageCode = "en-us",
downloadUsingCellular = DownloadUsingCellularSetting.ALWAYS,
streamingUsingCellular = StreamingUsingCellularSetting.ALWAYS,
@@ -188,9 +190,9 @@ data class DeviceSettings(
@JsonIgnore
fun getShakeThresholdGravity() : Float { // Used in ShakeDetector
return if (shakeSensitivity == ShakeSensitivitySetting.VERY_HIGH) 1.2f
else if (shakeSensitivity == ShakeSensitivitySetting.HIGH) 1.4f
else if (shakeSensitivity == ShakeSensitivitySetting.MEDIUM) 1.6f
return if (shakeSensitivity == ShakeSensitivitySetting.VERY_HIGH) 1.1f
else if (shakeSensitivity == ShakeSensitivitySetting.HIGH) 1.3f
else if (shakeSensitivity == ShakeSensitivitySetting.MEDIUM) 1.5f
else if (shakeSensitivity == ShakeSensitivitySetting.LOW) 2f
else if (shakeSensitivity == ShakeSensitivitySetting.VERY_LOW) 2.7f
else {
@@ -32,8 +32,9 @@ object DeviceManager {
var deviceData: DeviceData = dbManager.getDeviceData()
var serverConnectionConfig: ServerConnectionConfig? = null
val serverConnectionConfigId
get() = serverConnectionConfig?.id ?: ""
val serverConnectionConfigId get() = serverConnectionConfig?.id ?: ""
val serverConnectionConfigName get() = serverConnectionConfig?.name ?: ""
val serverConnectionConfigString get() = serverConnectionConfig?.name ?: "No server connection"
val serverAddress
get() = serverConnectionConfig?.address ?: ""
val serverUserId
@@ -63,6 +64,10 @@ object DeviceManager {
if (deviceData.deviceSettings?.autoSleepTimerAutoRewindTime == null) {
deviceData.deviceSettings?.autoSleepTimerAutoRewindTime = 300000L // 5 minutes
}
// Initialize sleep timer almost done chime added in v0.9.81
if (deviceData.deviceSettings?.enableSleepTimerAlmostDoneChime == null) {
deviceData.deviceSettings?.enableSleepTimerAlmostDoneChime = false
}
// Language added in v0.9.69
if (deviceData.deviceSettings?.languageCode == null) {
@@ -4,6 +4,8 @@ import android.content.Context
import android.util.Log
import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.models.DownloadItem
import com.audiobookshelf.app.plugins.AbsLog
import com.audiobookshelf.app.plugins.AbsLogger
import io.paperdb.Paper
import java.io.File
@@ -287,4 +289,35 @@ class DbManager {
}
return sessions
}
fun saveLog(log:AbsLog) {
Paper.book("log").write(log.id, log)
}
fun getAllLogs() : List<AbsLog> {
val logs:MutableList<AbsLog> = mutableListOf()
Paper.book("log").allKeys.forEach { logId ->
Paper.book("log").read<AbsLog>(logId)?.let {
logs.add(it)
}
}
return logs.sortedBy { it.timestamp }
}
fun removeAllLogs() {
Paper.book("log").destroy()
}
fun cleanLogs() {
val numberOfHoursToKeep = 48
val keepLogCutoff = System.currentTimeMillis() - (3600000 * numberOfHoursToKeep)
val allLogs = getAllLogs()
var logsRemoved = 0
allLogs.forEach {
if (it.timestamp < keepLogCutoff) {
Paper.book("log").delete(it.id)
logsRemoved++
}
}
if (logsRemoved > 0) {
AbsLogger.info("DbManager", "cleanLogs: Removed $logsRemoved logs older than $numberOfHoursToKeep hours")
}
}
}
@@ -29,7 +29,7 @@ class InternalDownloadManager(
*/
@Throws(IOException::class)
fun download(url: String) {
val request: Request = Request.Builder().url(url).build()
val request: Request = Request.Builder().url(url).addHeader("Accept-Encoding", "identity").build()
client.newCall(request)
.enqueue(
object : Callback {
@@ -1,15 +1,20 @@
package com.audiobookshelf.app.managers
import android.content.Context
import android.media.MediaPlayer
import android.os.*
import android.util.Log
import com.audiobookshelf.app.R
import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.player.PlayerNotificationService
import com.audiobookshelf.app.player.SLEEP_TIMER_WAKE_UP_EXPIRATION
import com.audiobookshelf.app.plugins.AbsLogger
import java.util.*
import kotlin.concurrent.schedule
import kotlin.math.roundToInt
const val SLEEP_TIMER_CHIME_SOUND_VOLUME = 0.7f
class SleepTimerManager
constructor(private val playerNotificationService: PlayerNotificationService) {
private val tag = "SleepTimerManager"
@@ -156,6 +161,10 @@ constructor(private val playerNotificationService: PlayerNotificationService) {
)
}
if (sleepTimeSecondsRemaining == 30 && sleepTimerElapsed > 1 && DeviceManager.deviceData.deviceSettings?.enableSleepTimerAlmostDoneChime == true) {
playChimeSound()
}
if (sleepTimeSecondsRemaining <= 0) {
Log.d(tag, "Sleep Timer Pausing Player on Chapter")
pause()
@@ -263,6 +272,18 @@ constructor(private val playerNotificationService: PlayerNotificationService) {
}
}
/** Plays chime sound */
private fun playChimeSound() {
AbsLogger.info(tag, "playChimeSound: Playing sleep timer chime sound")
val ctx = playerNotificationService.getContext()
val mediaPlayer = MediaPlayer.create(ctx, R.raw.bell)
mediaPlayer.setVolume(SLEEP_TIMER_CHIME_SOUND_VOLUME, SLEEP_TIMER_CHIME_SOUND_VOLUME)
mediaPlayer.start()
mediaPlayer.setOnCompletionListener {
mediaPlayer.release()
}
}
/**
* Gets the chapter end time for use in End of Chapter timers. If less than 10 seconds remain in
* the chapter, then use the next chapter.
@@ -136,7 +136,6 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
val serverConnConfig = if (DeviceManager.isConnectedToServer) DeviceManager.serverConnectionConfig else DeviceManager.deviceData.getLastServerConnectionConfig()
if (!DeviceManager.isConnectedToServer || !DeviceManager.checkConnectivity(ctx) || serverConnConfig == null || serverConnConfig.id !== serverConfigIdUsed) {
Log.d(tag, "Reset caches")
podcastEpisodeLibraryItemMap = mutableMapOf()
serverLibraries = listOf()
serverLibraryItems = mutableListOf()
@@ -8,6 +8,7 @@ import com.audiobookshelf.app.data.MediaProgress
import com.audiobookshelf.app.data.PlaybackSession
import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.player.PlayerNotificationService
import com.audiobookshelf.app.plugins.AbsLogger
import com.audiobookshelf.app.server.ApiHandler
import java.util.*
import kotlin.concurrent.schedule
@@ -208,6 +209,7 @@ class MediaProgressSyncer(
MediaEventManager.seekEvent(currentPlaybackSession!!, null)
}
// Currently unused
fun syncFromServerProgress(mediaProgress: MediaProgress) {
currentPlaybackSession?.let {
it.updatedAt = mediaProgress.lastUpdate
@@ -260,44 +262,46 @@ class MediaProgressSyncer(
tag,
"Sync local device current serverConnectionConfigId=${DeviceManager.serverConnectionConfig?.id}"
)
AbsLogger.info("MediaProgressSyncer", "sync: Saved local progress (title: \"$currentDisplayTitle\") (currentTime: $currentTime) (session id: ${it.id})")
// Local library item is linked to a server library item
// Send sync to server also if connected to this server and local item belongs to this
// server
val isConnectedToSameServer = it.serverConnectionConfigId != null && DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId
if (hasNetworkConnection &&
shouldSyncServer &&
!it.libraryItemId.isNullOrEmpty() &&
it.serverConnectionConfigId != null &&
DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId
isConnectedToSameServer
) {
apiHandler.sendLocalProgressSync(it) { syncSuccess, errorMsg ->
if (syncSuccess) {
failedSyncs = 0
playerNotificationService.alertSyncSuccess()
DeviceManager.dbManager.removePlaybackSession(it.id) // Remove session from db
AbsLogger.info("MediaProgressSyncer", "sync: Successfully synced local progress (title: \"$currentDisplayTitle\") (currentTime: $currentTime) (session id: ${it.id})")
} else {
failedSyncs++
if (failedSyncs == 2) {
playerNotificationService.alertSyncFailing() // Show alert in client
failedSyncs = 0
}
Log.e(
tag,
"Local Progress sync failed ($failedSyncs) to send to server $currentDisplayTitle for time $currentTime with session id=${it.id}"
)
AbsLogger.error("MediaProgressSyncer", "sync: Local progress sync failed (count: $failedSyncs) (title: \"$currentDisplayTitle\") (currentTime: $currentTime) (session id: ${it.id}) (${DeviceManager.serverConnectionConfigName})")
}
cb(SyncResult(true, syncSuccess, errorMsg))
}
} else {
AbsLogger.info("MediaProgressSyncer", "sync: Not sending local progress to server (title: \"$currentDisplayTitle\") (currentTime: $currentTime) (session id: ${it.id}) (hasNetworkConnection: $hasNetworkConnection) (isConnectedToSameServer: $isConnectedToSameServer)")
cb(SyncResult(false, null, null))
}
}
} else if (hasNetworkConnection && shouldSyncServer) {
Log.d(tag, "sync: currentSessionId=$currentSessionId")
AbsLogger.info("MediaProgressSyncer", "sync: Sending progress sync to server (title: \"$currentDisplayTitle\") (currentTime: $currentTime) (session id: ${currentSessionId}) (${DeviceManager.serverConnectionConfigName})")
apiHandler.sendProgressSync(currentSessionId, syncData) { syncSuccess, errorMsg ->
if (syncSuccess) {
Log.d(tag, "Progress sync data sent to server $currentDisplayTitle for time $currentTime")
AbsLogger.info("MediaProgressSyncer", "sync: Successfully synced progress (title: \"$currentDisplayTitle\") (currentTime: $currentTime) (session id: ${currentSessionId}) (${DeviceManager.serverConnectionConfigName})")
failedSyncs = 0
playerNotificationService.alertSyncSuccess()
lastSyncTime = System.currentTimeMillis()
@@ -308,14 +312,12 @@ class MediaProgressSyncer(
playerNotificationService.alertSyncFailing() // Show alert in client
failedSyncs = 0
}
Log.e(
tag,
"Progress sync failed ($failedSyncs) to send to server $currentDisplayTitle for time $currentTime with session id=${currentSessionId}"
)
AbsLogger.error("MediaProgressSyncer", "sync: Progress sync failed (count: $failedSyncs) (title: \"$currentDisplayTitle\") (currentTime: $currentTime) (session id: $currentSessionId) (${DeviceManager.serverConnectionConfigName})")
}
cb(SyncResult(true, syncSuccess, errorMsg))
}
} else {
AbsLogger.info("MediaProgressSyncer", "sync: Not sending progress to server (title: \"$currentDisplayTitle\") (currentTime: $currentTime) (session id: $currentSessionId) (${DeviceManager.serverConnectionConfigName}) (hasNetworkConnection: $hasNetworkConnection)")
cb(SyncResult(false, null, null))
}
}
@@ -7,7 +7,6 @@ import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import android.support.v4.media.session.MediaControllerCompat
import android.util.Log
import com.audiobookshelf.app.BuildConfig
import com.audiobookshelf.app.R
import com.bumptech.glide.Glide
@@ -41,7 +40,6 @@ class AbMediaDescriptionAdapter constructor(private val controller: MediaControl
// Cache the bitmap for the current audiobook so that successive calls to
// `getCurrentLargeIcon` don't cause the bitmap to be recreated.
currentIconUri = albumArtUri
Log.d(tag, "ART $currentIconUri")
if (currentIconUri.toString().startsWith("content://")) {
currentBitmap = if (Build.VERSION.SDK_INT < 28) {
@@ -8,7 +8,6 @@ import android.util.Log
import android.view.KeyEvent
import com.audiobookshelf.app.data.LibraryItemWrapper
import com.audiobookshelf.app.data.PodcastEpisode
import com.audiobookshelf.app.device.DeviceManager
import java.util.*
import kotlin.concurrent.schedule
@@ -4,14 +4,11 @@ import android.annotation.SuppressLint
import android.app.*
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.ImageDecoder
import android.hardware.Sensor
import android.hardware.SensorManager
import android.net.*
import android.os.*
import android.provider.MediaStore
import android.provider.Settings
import android.support.v4.media.MediaBrowserCompat
import android.support.v4.media.MediaDescriptionCompat
@@ -36,6 +33,7 @@ import com.audiobookshelf.app.media.MediaManager
import com.audiobookshelf.app.media.MediaProgressSyncer
import com.audiobookshelf.app.media.getUriToAbsIconDrawable
import com.audiobookshelf.app.media.getUriToDrawable
import com.audiobookshelf.app.plugins.AbsLogger
import com.audiobookshelf.app.server.ApiHandler
import com.google.android.exoplayer2.*
import com.google.android.exoplayer2.audio.AudioAttributes
@@ -310,19 +308,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
val coverUri = currentPlaybackSession!!.getCoverUri(ctx)
var bitmap: Bitmap? = null
// Local covers get bitmap
if (currentPlaybackSession!!.localLibraryItem?.coverContentUrl != null) {
bitmap =
if (Build.VERSION.SDK_INT < 28) {
MediaStore.Images.Media.getBitmap(ctx.contentResolver, coverUri)
} else {
val source: ImageDecoder.Source =
ImageDecoder.createSource(ctx.contentResolver, coverUri)
ImageDecoder.decodeBitmap(source)
}
}
// Fix for local images crashing on Android 11 for specific devices
// https://stackoverflow.com/questions/64186578/android-11-mediastyle-notification-crash/64232958#64232958
try {
@@ -346,8 +331,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
.setExtras(extra)
.setTitle(currentPlaybackSession!!.displayTitle)
bitmap?.let { mediaDescriptionBuilder.setIconBitmap(it) }
?: mediaDescriptionBuilder.setIconUri(coverUri)
mediaDescriptionBuilder.setIconUri(coverUri)
return mediaDescriptionBuilder.build()
}
@@ -452,7 +436,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
playbackSession
) // Save playback session to use when app is closed
Log.d(tag, "Set CurrentPlaybackSession MediaPlayer ${currentPlaybackSession?.mediaPlayer}")
AbsLogger.info("PlayerNotificationService", "preparePlayer: Started playback session for item ${currentPlaybackSession?.mediaItemId}. MediaPlayer ${currentPlaybackSession?.mediaPlayer}")
// Notify client
clientEventEmitter?.onPlaybackSession(playbackSession)
@@ -469,7 +453,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
val mediaSource: MediaSource
if (playbackSession.isLocal) {
Log.d(tag, "Playing Local Item")
AbsLogger.info("PlayerNotificationService", "preparePlayer: Playing local item ${currentPlaybackSession?.mediaItemId}.")
val dataSourceFactory = DefaultDataSource.Factory(ctx)
val extractorsFactory = DefaultExtractorsFactory()
@@ -483,7 +467,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory)
.createMediaSource(mediaItems[0])
} else if (!playbackSession.isHLS) {
Log.d(tag, "Direct Playing Item")
AbsLogger.info("PlayerNotificationService", "preparePlayer: Direct playing item ${currentPlaybackSession?.mediaItemId}.")
val dataSourceFactory = DefaultHttpDataSource.Factory()
val extractorsFactory = DefaultExtractorsFactory()
@@ -498,7 +482,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory)
.createMediaSource(mediaItems[0])
} else {
Log.d(tag, "Playing HLS Item")
AbsLogger.info("PlayerNotificationService", "preparePlayer: Playing HLS stream of item ${currentPlaybackSession?.mediaItemId}.")
val dataSourceFactory = DefaultHttpDataSource.Factory()
dataSourceFactory.setUserAgent(channelId)
dataSourceFactory.setDefaultRequestProperties(
@@ -1105,11 +1089,12 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
// No further calls will be made to other media browsing methods.
null
} else {
Log.d(tag, "Android Auto starting $clientPackageName $clientUid")
AbsLogger.info(tag, "onGetRoot: clientPackageName: $clientPackageName, clientUid: $clientUid")
isStarted = true
// Reset cache if no longer connected to server or server changed
if (mediaManager.checkResetServerItems()) {
AbsLogger.info(tag, "onGetRoot: Reset Android Auto server items cache (${DeviceManager.serverConnectionConfigString})")
forceReloadingAndroidAuto = true
}
@@ -1134,7 +1119,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
parentMediaId: String,
result: Result<MutableList<MediaBrowserCompat.MediaItem>>
) {
Log.d(tag, "ON LOAD CHILDREN $parentMediaId")
AbsLogger.info(tag, "onLoadChildren: parentMediaId: $parentMediaId (${DeviceManager.serverConnectionConfigString})")
result.detach()
@@ -1145,7 +1130,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
}
if (parentMediaId == DOWNLOADS_ROOT) { // Load downloads
val localBooks = DeviceManager.dbManager.getLocalLibraryItems("book")
val localPodcasts = DeviceManager.dbManager.getLocalLibraryItems("podcast")
val localBrowseItems: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
@@ -1242,8 +1226,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
Log.d(tag, "Trying to initialize browseTree.")
if (!this::browseTree.isInitialized || forceReloadingAndroidAuto) {
forceReloadingAndroidAuto = false
AbsLogger.info(tag, "onLoadChildren: Loading Android Auto items")
mediaManager.loadAndroidAutoItems {
Log.d(tag, "android auto loaded. Starting browseTree initialize")
AbsLogger.info(tag, "onLoadChildren: Loaded Android Auto data, initializing browseTree")
browseTree =
BrowseTree(
this,
@@ -1259,15 +1245,21 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
)
}
Log.d(tag, "browseTree initialize and android auto loaded")
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
firstLoadDone = true
if (mediaManager.serverLibraries.isNotEmpty()) {
Log.d(tag, "Starting personalization fetch")
mediaManager.populatePersonalizedDataForAllLibraries { notifyChildrenChanged("/") }
AbsLogger.info(tag, "onLoadChildren: Android Auto fetching personalized data for all libraries")
mediaManager.populatePersonalizedDataForAllLibraries {
AbsLogger.info(tag, "onLoadChildren: Android Auto loaded personalized data for all libraries")
notifyChildrenChanged("/")
}
Log.d(tag, "Initialize inprogress items")
mediaManager.initializeInProgressItems { notifyChildrenChanged("/") }
AbsLogger.info(tag, "onLoadChildren: Android Auto fetching in progress items")
mediaManager.initializeInProgressItems {
AbsLogger.info(tag, "onLoadChildren: Android Auto loaded in progress items")
notifyChildrenChanged("/")
}
}
}
} else {
@@ -1287,7 +1279,8 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
)
}
Log.d(tag, "browseTree initialize and android auto loaded")
AbsLogger.info(tag, "onLoadChildren: Android auto data loaded")
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
}
} else if (parentMediaId == LIBRARIES_ROOT || parentMediaId == RECENTLY_ROOT) {
@@ -5,6 +5,7 @@ import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.plugins.AbsLogger
import kotlin.math.sqrt
class ShakeDetector : SensorEventListener {
@@ -46,6 +47,7 @@ class ShakeDetector : SensorEventListener {
if (mShakeTimestamp + SHAKE_COUNT_RESET_TIME_MS < now) {
mShakeCount = 0
}
AbsLogger.info("ShakeDetector", "Device shake above threshold ($gForce > $shakeThreshold)")
mShakeTimestamp = now
mShakeCount++
mListener!!.onShake(mShakeCount)
@@ -180,7 +180,8 @@ class AbsAudioPlayer : Plugin() {
val playWhenReady = call.getBoolean("playWhenReady") == true
val playbackRate = call.getFloat("playbackRate",1f) ?: 1f
val startTimeOverride = call.getDouble("startTime")
Log.d(tag, "prepareLibraryItem lid=$libraryItemId, startTimeOverride=$startTimeOverride, playbackRate=$playbackRate")
AbsLogger.info("AbsAudioPlayer", "prepareLibraryItem: lid=$libraryItemId, startTimeOverride=$startTimeOverride, playbackRate=$playbackRate")
if (libraryItemId.isEmpty()) {
Log.e(tag, "Invalid call to play library item no library item id")
@@ -36,6 +36,7 @@ class AbsDatabase : Plugin() {
DeviceManager.dbManager.cleanLocalMediaProgress()
DeviceManager.dbManager.cleanLocalLibraryItems()
DeviceManager.dbManager.cleanLogs()
}
@PluginMethod
@@ -220,12 +221,11 @@ class AbsDatabase : Plugin() {
@PluginMethod
fun syncLocalSessionsWithServer(call:PluginCall) {
if (DeviceManager.serverConnectionConfig == null) {
Log.e(tag, "syncLocalSessionsWithServer not connected to server")
AbsLogger.error("AbsDatabase", "syncLocalSessionsWithServer: not connected to server")
return call.resolve()
}
apiHandler.syncLocalMediaProgressForUser {
Log.d(tag, "Finished syncing local media progress for user")
val savedSessions = DeviceManager.dbManager.getPlaybackSessions().filter { it.serverConnectionConfigId == DeviceManager.serverConnectionConfigId }
if (savedSessions.isNotEmpty()) {
@@ -233,6 +233,7 @@ class AbsDatabase : Plugin() {
if (!success) {
call.resolve(JSObject("{\"error\":\"$errorMsg\"}"))
} else {
AbsLogger.info("AbsDatabase", "syncLocalSessionsWithServer: Finished sending local playback sessions to server. Removing ${savedSessions.size} saved sessions.")
// Remove all local sessions
savedSessions.forEach {
DeviceManager.dbManager.removePlaybackSession(it.id)
@@ -241,6 +242,7 @@ class AbsDatabase : Plugin() {
}
}
} else {
AbsLogger.info("AbsDatabase", "syncLocalSessionsWithServer: No saved local playback sessions to send to server.")
call.resolve()
}
}
@@ -0,0 +1,80 @@
package com.audiobookshelf.app.plugins
import android.util.Log
import com.audiobookshelf.app.device.DeviceManager
import com.fasterxml.jackson.core.json.JsonReadFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.getcapacitor.JSObject
import com.getcapacitor.Plugin
import com.getcapacitor.PluginCall
import com.getcapacitor.PluginMethod
import com.getcapacitor.annotation.CapacitorPlugin
import java.util.UUID
data class AbsLog(
var id:String,
var tag:String,
var level:String,
var message:String,
var timestamp:Long
)
data class AbsLogList(val value:List<AbsLog>)
@CapacitorPlugin(name = "AbsLogger")
class AbsLogger : Plugin() {
private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
override fun load() {
onLogEmitter = { log:AbsLog ->
notifyListeners("onLog", JSObject(jacksonMapper.writeValueAsString(log)))
}
info("AbsLogger", "load: AbsLogger plugin initialized")
}
companion object {
var onLogEmitter:((log:AbsLog) -> Unit)? = null
fun log(level:String, tag:String, message:String) {
val absLog = AbsLog(id = UUID.randomUUID().toString(), tag, level, message, timestamp = System.currentTimeMillis())
DeviceManager.dbManager.saveLog(absLog)
onLogEmitter?.let { it(absLog) }
}
fun info(tag:String, message:String) {
Log.i("AbsLogger", message)
log("info", tag, message)
}
fun error(tag:String, message:String) {
Log.e("AbsLogger", message)
log("error", tag, message)
}
}
@PluginMethod
fun info(call: PluginCall) {
val msg = call.getString("message") ?: return call.reject("No message")
val tag = call.getString("tag") ?: ""
info(tag, msg)
call.resolve()
}
@PluginMethod
fun error(call: PluginCall) {
val msg = call.getString("message") ?: return call.reject("No message")
val tag = call.getString("tag") ?: ""
error(tag, msg)
call.resolve()
}
@PluginMethod
fun getAllLogs(call: PluginCall) {
val absLogs = DeviceManager.dbManager.getAllLogs()
call.resolve(JSObject(jacksonMapper.writeValueAsString(AbsLogList(absLogs))))
}
@PluginMethod
fun clearLogs(call: PluginCall) {
DeviceManager.dbManager.removeAllLogs()
call.resolve()
}
}
@@ -13,6 +13,7 @@ import com.audiobookshelf.app.media.MediaProgressSyncData
import com.audiobookshelf.app.media.SyncResult
import com.audiobookshelf.app.models.User
import com.audiobookshelf.app.BuildConfig
import com.audiobookshelf.app.plugins.AbsLogger
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.core.json.JsonReadFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
@@ -468,22 +469,27 @@ class ApiHandler(var ctx:Context) {
val deviceInfo = DeviceInfo(deviceId, Build.MANUFACTURER, Build.MODEL, Build.VERSION.SDK_INT, BuildConfig.VERSION_NAME)
val payload = JSObject(jacksonMapper.writeValueAsString(LocalSessionsSyncRequestPayload(playbackSessions, deviceInfo)))
Log.d(tag, "Sending ${playbackSessions.size} saved local playback sessions to server")
AbsLogger.info("ApiHandler", "sendSyncLocalSessions: Sending ${playbackSessions.size} saved local playback sessions to server (${DeviceManager.serverConnectionConfigName})")
postRequest("/api/session/local-all", payload, null) {
if (!it.getString("error").isNullOrEmpty()) {
Log.e(tag, "Failed to sync local sessions")
AbsLogger.error("ApiHandler", "sendSyncLocalSessions: Failed to sync local sessions. (${it.getString("error")})")
cb(false, it.getString("error"))
} else {
val response = jacksonMapper.readValue<LocalSessionsSyncResponsePayload>(it.toString())
response.results.forEach { localSessionSyncResult ->
Log.d(tag, "Synced session result ${localSessionSyncResult.id}|${localSessionSyncResult.progressSynced}|${localSessionSyncResult.success}")
playbackSessions.find { ps -> ps.id == localSessionSyncResult.id }?.let { session ->
if (localSessionSyncResult.progressSynced == true) {
val syncResult = SyncResult(true, true, "Progress synced on server")
MediaEventManager.saveEvent(session, syncResult)
Log.i(tag, "Successfully synced session ${session.displayTitle} with server")
AbsLogger.info("ApiHandler", "sendSyncLocalSessions: Synced session \"${session.displayTitle}\" with server, server progress was updated for item ${session.mediaItemId}")
} else if (!localSessionSyncResult.success) {
Log.e(tag, "Failed to sync session ${session.displayTitle} with server. Error: ${localSessionSyncResult.error}")
AbsLogger.error("ApiHandler", "sendSyncLocalSessions: Failed to sync session \"${session.displayTitle}\" with server. Error: ${localSessionSyncResult.error}")
} else {
AbsLogger.info("ApiHandler", "sendSyncLocalSessions: Synced session \"${session.displayTitle}\" with server. Server progress was up-to-date for item ${session.mediaItemId}")
}
}
}
@@ -493,37 +499,72 @@ class ApiHandler(var ctx:Context) {
}
fun syncLocalMediaProgressForUser(cb: () -> Unit) {
AbsLogger.info("ApiHandler", "[ApiHandler] syncLocalMediaProgressForUser: Server connection ${DeviceManager.serverConnectionConfigName}")
// Get all local media progress for this server
val allLocalMediaProgress = DeviceManager.dbManager.getAllLocalMediaProgress().filter { it.serverConnectionConfigId == DeviceManager.serverConnectionConfigId }
if (allLocalMediaProgress.isEmpty()) {
Log.d(tag, "No local media progress to sync")
AbsLogger.info("ApiHandler", "[ApiHandler] syncLocalMediaProgressForUser: No local media progress to sync")
return cb()
}
getCurrentUser { _user ->
_user?.let { user->
AbsLogger.info("ApiHandler", "syncLocalMediaProgressForUser: Found ${allLocalMediaProgress.size} local media progress")
getCurrentUser { user ->
if (user == null) {
AbsLogger.error("ApiHandler", "syncLocalMediaProgressForUser: Failed to load user from server (${DeviceManager.serverConnectionConfigName})")
} else {
var numLocalMediaProgressUptToDate = 0
var numLocalMediaProgressUpdated = 0
// Compare server user progress with local progress
user.mediaProgress.forEach { mediaProgress ->
// Get matching local media progress
allLocalMediaProgress.find { it.isMatch(mediaProgress) }?.let { localMediaProgress ->
if (mediaProgress.lastUpdate > localMediaProgress.lastUpdate) {
Log.d(tag, "Server progress for media item id=\"${mediaProgress.mediaItemId}\" is more recent then local. Updating local current time ${localMediaProgress.currentTime} to ${mediaProgress.currentTime}")
val updateLogs = mutableListOf<String>()
if (mediaProgress.progress != localMediaProgress.progress) {
updateLogs.add("Updated progress from ${localMediaProgress.progress} to ${mediaProgress.progress}")
}
if (mediaProgress.currentTime != localMediaProgress.currentTime) {
updateLogs.add("Updated currentTime from ${localMediaProgress.currentTime} to ${mediaProgress.currentTime}")
}
if (mediaProgress.isFinished != localMediaProgress.isFinished) {
updateLogs.add("Updated isFinished from ${localMediaProgress.isFinished} to ${mediaProgress.isFinished}")
}
if (mediaProgress.ebookProgress != localMediaProgress.ebookProgress) {
updateLogs.add("Updated ebookProgress from ${localMediaProgress.isFinished} to ${mediaProgress.isFinished}")
}
if (updateLogs.isNotEmpty()) {
AbsLogger.info("ApiHandler", "syncLocalMediaProgressForUser: Server progress for item \"${mediaProgress.mediaItemId}\" is more recent than local (server lastUpdate=${mediaProgress.lastUpdate}, local lastUpdate=${localMediaProgress.lastUpdate}). ${updateLogs.joinToString()}")
}
localMediaProgress.updateFromServerMediaProgress(mediaProgress)
MediaEventManager.syncEvent(mediaProgress, "Sync on server connection")
// Only report sync if progress changed
if (updateLogs.isNotEmpty()) {
MediaEventManager.syncEvent(mediaProgress, "Sync on server connection")
}
DeviceManager.dbManager.saveLocalMediaProgress(localMediaProgress)
numLocalMediaProgressUpdated++
} else if (localMediaProgress.lastUpdate > mediaProgress.lastUpdate && localMediaProgress.ebookLocation != null && localMediaProgress.ebookLocation != mediaProgress.ebookLocation) {
// Patch ebook progress to server
AbsLogger.info("ApiHandler", "syncLocalMediaProgressForUser: Local progress for ebook item \"${mediaProgress.mediaItemId}\" is more recent than server progress. Local progress last updated ${localMediaProgress.lastUpdate}, server progress last updated ${mediaProgress.lastUpdate}. Sending server request to update ebook progress from ${mediaProgress.ebookProgress} to ${localMediaProgress.ebookProgress}")
val endpoint = "/api/me/progress/${localMediaProgress.libraryItemId}"
val updatePayload = JSObject()
updatePayload.put("ebookLocation", localMediaProgress.ebookLocation)
updatePayload.put("ebookProgress", localMediaProgress.ebookProgress)
updatePayload.put("lastUpdate", localMediaProgress.lastUpdate)
patchRequest(endpoint,updatePayload) {
Log.d(tag, "syncLocalMediaProgressForUser patched ebook progress")
AbsLogger.info("ApiHandler", "syncLocalMediaProgressForUser: Successfully updated server ebook progress for item item \"${mediaProgress.mediaItemId}\"")
}
} else {
numLocalMediaProgressUptToDate++
}
}
}
AbsLogger.info("ApiHandler", "syncLocalMediaProgressForUser: Finishing syncing local media progress with server. $numLocalMediaProgressUptToDate up-to-date, $numLocalMediaProgressUpdated updated")
}
cb()
}
Binary file not shown.
+1 -1
View File
@@ -14,7 +14,7 @@ ext {
androidx_core_ktx_version = '1.16.0'
androidx_media_version = '1.7.0'
exoplayer_version = '2.18.7'
glide_version = '4.11.0'
glide_version = '4.16.0'
junit_version = '4.13.2'
kotlin_version = '2.1.0'
kotlin_coroutines_version = '1.10.1'
+1 -4
View File
@@ -7,7 +7,7 @@
<a v-if="showBack" @click="back" class="rounded-full h-10 w-10 flex items-center justify-center mr-2 cursor-pointer">
<span class="material-symbols text-3xl text-fg">arrow_back</span>
</a>
<div v-if="user && currentLibrary && socketConnected">
<div v-if="user && currentLibrary">
<div class="pl-1.5 pr-2.5 py-2 bg-bg bg-opacity-30 rounded-md flex items-center" @click="clickShowLibraryModal">
<ui-library-icon :icon="currentLibraryIcon" :size="4" font-size="base" />
<p class="text-sm leading-4 ml-2 mt-0.5 max-w-24 truncate">{{ currentLibraryName }}</p>
@@ -56,9 +56,6 @@ export default {
this.$store.commit('setCastAvailable', val)
}
},
socketConnected() {
return this.$store.state.socketConnected
},
currentLibrary() {
return this.$store.getters['libraries/getCurrentLibrary']
},
+2 -1
View File
@@ -9,7 +9,7 @@
</template>
<script>
import { AbsAudioPlayer } from '@/plugins/capacitor'
import { AbsAudioPlayer, AbsLogger } from '@/plugins/capacitor'
import { Dialog } from '@capacitor/dialog'
import CellularPermissionHelpers from '@/mixins/cellularPermissionHelpers'
@@ -190,6 +190,7 @@ export default {
})
},
async playLibraryItem(payload) {
await AbsLogger.info({ tag: 'AudioPlayerContainer', message: `playLibraryItem: Received play request for library item ${payload.libraryItemId} ${payload.episodeId ? `episode ${payload.episodeId}` : ''}` })
const libraryItemId = payload.libraryItemId
const episodeId = payload.episodeId
const startTime = payload.startTime
+9
View File
@@ -134,6 +134,15 @@ export default {
to: '/settings'
})
if (this.$platform !== 'ios') {
items.push({
icon: 'bug_report',
iconOutlined: true,
text: this.$strings.ButtonLogs,
to: '/logs'
})
}
if (this.serverConnectionConfig) {
items.push({
icon: 'language',
+2 -2
View File
@@ -77,12 +77,12 @@
</div>
<!-- Series sequence -->
<div v-if="seriesSequence && showSequence && !isSelectionMode" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
<div v-if="seriesSequence && showSequence && !isSelectionMode" class="absolute rounded-lg bg-black/90 text-white box-shadow-md z-10" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">#{{ seriesSequence }}</p>
</div>
<!-- Podcast Episode # -->
<div v-if="recentEpisodeNumber !== null && !isSelectionMode" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
<div v-if="recentEpisodeNumber !== null && !isSelectionMode" class="absolute rounded-lg bg-black/90 box-shadow-md z-10" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
<p class="text-white" :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">
Episode<span v-if="recentEpisodeNumber"> #{{ recentEpisodeNumber }}</span>
</p>
+8 -20
View File
@@ -15,20 +15,17 @@
3ABF580928059BAE005DFBE5 /* PlaybackSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ABF580828059BAE005DFBE5 /* PlaybackSession.swift */; };
3ABF618F2804325C0070250E /* PlayerHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ABF618E2804325C0070250E /* PlayerHandler.swift */; };
3AD4FCE528043E50006DB301 /* AbsDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AD4FCE428043E50006DB301 /* AbsDatabase.swift */; };
3AD4FCE728043E72006DB301 /* AbsDatabase.m in Sources */ = {isa = PBXBuildFile; fileRef = 3AD4FCE628043E72006DB301 /* AbsDatabase.m */; };
3AD4FCE928043FD7006DB301 /* ServerConnectionConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AD4FCE828043FD7006DB301 /* ServerConnectionConfig.swift */; };
3AD4FCEB280443DD006DB301 /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AD4FCEA280443DD006DB301 /* Database.swift */; };
3AD4FCED28044E6C006DB301 /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AD4FCEC28044E6C006DB301 /* Store.swift */; };
3AF1970C2806E2590096F747 /* ApiClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF1970B2806E2590096F747 /* ApiClient.swift */; };
3AF1970E2806E3CA0096F747 /* AbsAudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF1970D2806E3CA0096F747 /* AbsAudioPlayer.swift */; };
3AF197102806E3DC0096F747 /* AbsAudioPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 3AF1970F2806E3DC0096F747 /* AbsAudioPlayer.m */; };
3AFCB5E827EA240D00ECCC05 /* NowPlayingInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AFCB5E727EA240D00ECCC05 /* NowPlayingInfo.swift */; };
4D66B952282EE822008272D4 /* AbsDownloader.m in Sources */ = {isa = PBXBuildFile; fileRef = 4D66B951282EE822008272D4 /* AbsDownloader.m */; };
4D66B954282EE87C008272D4 /* AbsDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D66B953282EE87C008272D4 /* AbsDownloader.swift */; };
4D66B956282EE951008272D4 /* AbsFileSystem.m in Sources */ = {isa = PBXBuildFile; fileRef = 4D66B955282EE951008272D4 /* AbsFileSystem.m */; };
4D66B958282EEA14008272D4 /* AbsFileSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D66B957282EEA14008272D4 /* AbsFileSystem.swift */; };
4D91EEC62A40F28D004807ED /* EBookFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D91EEC52A40F28D004807ED /* EBookFile.swift */; };
4DABC04F2B0139CA000F6264 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DABC04E2B0139CA000F6264 /* User.swift */; };
4DF6C7172DB58ABF004059F1 /* AbsLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DF6C7162DB58ABF004059F1 /* AbsLogger.swift */; };
4DF74912287105C600AC7814 /* DeviceSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DF74911287105C600AC7814 /* DeviceSettings.swift */; };
4DFE2DA32D345C390000B204 /* MyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DFE2DA22D345C390000B204 /* MyViewController.swift */; };
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; };
@@ -90,21 +87,18 @@
3ABF580828059BAE005DFBE5 /* PlaybackSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackSession.swift; sourceTree = "<group>"; };
3ABF618E2804325C0070250E /* PlayerHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerHandler.swift; sourceTree = "<group>"; };
3AD4FCE428043E50006DB301 /* AbsDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbsDatabase.swift; sourceTree = "<group>"; };
3AD4FCE628043E72006DB301 /* AbsDatabase.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AbsDatabase.m; sourceTree = "<group>"; };
3AD4FCE828043FD7006DB301 /* ServerConnectionConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConnectionConfig.swift; sourceTree = "<group>"; };
3AD4FCEA280443DD006DB301 /* Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Database.swift; sourceTree = "<group>"; };
3AD4FCEC28044E6C006DB301 /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = "<group>"; };
3AF1970B2806E2590096F747 /* ApiClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiClient.swift; sourceTree = "<group>"; };
3AF1970D2806E3CA0096F747 /* AbsAudioPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbsAudioPlayer.swift; sourceTree = "<group>"; };
3AF1970F2806E3DC0096F747 /* AbsAudioPlayer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AbsAudioPlayer.m; sourceTree = "<group>"; };
3AFCB5E727EA240D00ECCC05 /* NowPlayingInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingInfo.swift; sourceTree = "<group>"; };
4D66B951282EE822008272D4 /* AbsDownloader.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AbsDownloader.m; sourceTree = "<group>"; };
4D66B953282EE87C008272D4 /* AbsDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbsDownloader.swift; sourceTree = "<group>"; };
4D66B955282EE951008272D4 /* AbsFileSystem.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AbsFileSystem.m; sourceTree = "<group>"; };
4D66B957282EEA14008272D4 /* AbsFileSystem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbsFileSystem.swift; sourceTree = "<group>"; };
4D8D412C26E187E400BA5F0D /* App-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "App-Bridging-Header.h"; sourceTree = "<group>"; };
4D91EEC52A40F28D004807ED /* EBookFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EBookFile.swift; sourceTree = "<group>"; };
4DABC04E2B0139CA000F6264 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = "<group>"; };
4DF6C7162DB58ABF004059F1 /* AbsLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbsLogger.swift; sourceTree = "<group>"; };
4DF74911287105C600AC7814 /* DeviceSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceSettings.swift; sourceTree = "<group>"; };
4DFE2DA22D345C390000B204 /* MyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyViewController.swift; sourceTree = "<group>"; };
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
@@ -217,13 +211,10 @@
isa = PBXGroup;
children = (
3AD4FCE428043E50006DB301 /* AbsDatabase.swift */,
3AD4FCE628043E72006DB301 /* AbsDatabase.m */,
3AF1970D2806E3CA0096F747 /* AbsAudioPlayer.swift */,
3AF1970F2806E3DC0096F747 /* AbsAudioPlayer.m */,
4D66B951282EE822008272D4 /* AbsDownloader.m */,
4D66B953282EE87C008272D4 /* AbsDownloader.swift */,
4D66B955282EE951008272D4 /* AbsFileSystem.m */,
4D66B957282EEA14008272D4 /* AbsFileSystem.swift */,
4DF6C7162DB58ABF004059F1 /* AbsLogger.swift */,
);
path = plugins;
sourceTree = "<group>";
@@ -531,7 +522,6 @@
files = (
E9D5507328AC218300C746DD /* DaoExtensions.swift in Sources */,
E9D5506228AC1CC900C746DD /* PlayerState.swift in Sources */,
3AD4FCE728043E72006DB301 /* AbsDatabase.m in Sources */,
504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
EACB38122BCCA1330060DA4A /* AudioPlayerRateManager.swift in Sources */,
E9FA07E328C82848005520B0 /* Logger.swift in Sources */,
@@ -545,6 +535,7 @@
4D66B958282EEA14008272D4 /* AbsFileSystem.swift in Sources */,
E9D5504C28AC1AE000C746DD /* PodcastEpisode.swift in Sources */,
E9D5506A28AC1DF100C746DD /* LocalFile.swift in Sources */,
4DF6C7172DB58ABF004059F1 /* AbsLogger.swift in Sources */,
3AF1970E2806E3CA0096F747 /* AbsAudioPlayer.swift in Sources */,
E9D5506F28AC1E8E00C746DD /* DownloadItem.swift in Sources */,
3AD4FCE928043FD7006DB301 /* ServerConnectionConfig.swift in Sources */,
@@ -553,7 +544,6 @@
3A200C1527D64D7E00CBF02E /* AudioPlayer.swift in Sources */,
4DFE2DA32D345C390000B204 /* MyViewController.swift in Sources */,
E9D5507128AC1EC700C746DD /* DownloadItemPart.swift in Sources */,
4D66B956282EE951008272D4 /* AbsFileSystem.m in Sources */,
EACB38142BCCA1410060DA4A /* LegacyAudioPlayerRateManager.swift in Sources */,
3AFCB5E827EA240D00ECCC05 /* NowPlayingInfo.swift in Sources */,
3AB34053280829BF0039308B /* Extensions.swift in Sources */,
@@ -561,7 +551,6 @@
3AD4FCEB280443DD006DB301 /* Database.swift in Sources */,
3AD4FCE528043E50006DB301 /* AbsDatabase.swift in Sources */,
4DABC04F2B0139CA000F6264 /* User.swift in Sources */,
4D66B952282EE822008272D4 /* AbsDownloader.m in Sources */,
E9D5506828AC1DC300C746DD /* LocalPodcastEpisode.swift in Sources */,
EACB38162BCCA1500060DA4A /* DefaultedAudioPlayerRateManager.swift in Sources */,
E9D5505228AC1B5D00C746DD /* Chapter.swift in Sources */,
@@ -574,7 +563,6 @@
E9D5505C28AC1C6200C746DD /* LibraryFile.swift in Sources */,
4DF74912287105C600AC7814 /* DeviceSettings.swift in Sources */,
E9D5504A28AC1AA600C746DD /* Metadata.swift in Sources */,
3AF197102806E3DC0096F747 /* AbsAudioPlayer.m in Sources */,
E9D5507528AEF93100C746DD /* PlayerSettings.swift in Sources */,
E9D5505028AC1B3E00C746DD /* Author.swift in Sources */,
3AF1970C2806E2590096F747 /* ApiClient.swift in Sources */,
@@ -744,12 +732,12 @@
ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 36;
CURRENT_PROJECT_VERSION = 38;
DEVELOPMENT_TEAM = 7UFJ7D8V6A;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 0.9.79;
MARKETING_VERSION = 0.9.81;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app.dev;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -768,12 +756,12 @@
ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 36;
CURRENT_PROJECT_VERSION = 38;
DEVELOPMENT_TEAM = 7UFJ7D8V6A;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 0.9.79;
MARKETING_VERSION = 0.9.81;
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
+1
View File
@@ -21,6 +21,7 @@ class MyViewController: CAPBridgeViewController {
bridge?.registerPluginInstance(AbsAudioPlayer())
bridge?.registerPluginInstance(AbsDownloader())
bridge?.registerPluginInstance(AbsFileSystem())
bridge?.registerPluginInstance(AbsLogger())
}
-35
View File
@@ -1,35 +0,0 @@
//
// AbsAudioPlayer.m
// App
//
// Created by Rasmus Krämer on 13.04.22.
//
#import <Foundation/Foundation.h>
#import <Capacitor/Capacitor.h>
CAP_PLUGIN(AbsAudioPlayer, "AbsAudioPlayer",
CAP_PLUGIN_METHOD(onReady, CAPPluginReturnNone);
CAP_PLUGIN_METHOD(prepareLibraryItem, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(closePlayback, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(setPlaybackSpeed, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(setChapterTrack, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(playPlayer, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(pausePlayer, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(playPause, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(seek, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(seekForward, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(seekBackward, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(getCurrentTime, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(cancelSleepTimer, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(decreaseSleepTime, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(increaseSleepTime, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(getSleepTimerTime, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(setSleepTimer, CAPPluginReturnPromise);
)
+23 -1
View File
@@ -11,7 +11,29 @@ import RealmSwift
import Network
@objc(AbsAudioPlayer)
public class AbsAudioPlayer: CAPPlugin {
public class AbsAudioPlayer: CAPPlugin, CAPBridgedPlugin {
public var identifier = "AbsAudioPlayerPlugin"
public var jsName = "AbsAudioPlayer"
public let pluginMethods: [CAPPluginMethod] = [
CAPPluginMethod(name: "onReady", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "prepareLibraryItem", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "closePlayback", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "setPlaybackSpeed", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "setChapterTrack", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "playPlayer", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "pausePlayer", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "playPause", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "seek", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "seekForward", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "seekBackward", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "getCurrentTime", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "cancelSleepTimer", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "decreaseSleepTime", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "increaseSleepTime", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "getSleepTimerTime", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "setSleepTimer", returnType: CAPPluginReturnPromise)
]
private let logger = AppLogger(category: "AbsAudioPlayer")
private var initialPlayWhenReady = false
-29
View File
@@ -1,29 +0,0 @@
//
// AbsDatabase.m
// App
//
// Created by Rasmus Krämer on 11.04.22.
//
#import <Foundation/Foundation.h>
#import <Capacitor/Capacitor.h>
CAP_PLUGIN(AbsDatabase, "AbsDatabase",
CAP_PLUGIN_METHOD(setCurrentServerConnectionConfig, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(removeServerConnectionConfig, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(logout, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(getDeviceData, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(getLocalLibraryItems, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(getLocalLibraryItem, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(getLocalLibraryItemByLId, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(getLocalLibraryItemsInFolder, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(getAllLocalMediaProgress, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(removeLocalMediaProgress, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(syncServerMediaProgressWithLocalMediaProgress, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(syncLocalSessionsWithServer, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(updateLocalMediaProgressFinished, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(updateDeviceSettings, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(updateLocalEbookProgress, CAPPluginReturnPromise);
)
+21 -1
View File
@@ -27,7 +27,27 @@ extension String {
}
@objc(AbsDatabase)
public class AbsDatabase: CAPPlugin {
public class AbsDatabase: CAPPlugin, CAPBridgedPlugin {
public var identifier = "AbsDatabasePlugin"
public var jsName = "AbsDatabase"
public let pluginMethods: [CAPPluginMethod] = [
CAPPluginMethod(name: "setCurrentServerConnectionConfig", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "removeServerConnectionConfig", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "logout", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "getDeviceData", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "getLocalLibraryItems", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "getLocalLibraryItem", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "getLocalLibraryItemByLId", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "getLocalLibraryItemsInFolder", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "getAllLocalMediaProgress", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "removeLocalMediaProgress", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "syncServerMediaProgressWithLocalMediaProgress", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "syncLocalSessionsWithServer", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "updateLocalMediaProgressFinished", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "updateDeviceSettings", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "updateLocalEbookProgress", returnType: CAPPluginReturnPromise)
]
private let logger = AppLogger(category: "AbsDatabase")
@objc func setCurrentServerConnectionConfig(_ call: CAPPluginCall) {
-13
View File
@@ -1,13 +0,0 @@
//
// AbsDownloader.m
// App
//
// Created by advplyr on 5/13/22.
//
#import <Foundation/Foundation.h>
#import <Capacitor/Capacitor.h>
CAP_PLUGIN(AbsDownloader, "AbsDownloader",
CAP_PLUGIN_METHOD(downloadLibraryItem, CAPPluginReturnPromise);
)
+6 -1
View File
@@ -10,7 +10,12 @@ import Capacitor
import RealmSwift
@objc(AbsDownloader)
public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
public class AbsDownloader: CAPPlugin, CAPBridgedPlugin, URLSessionDownloadDelegate {
public var identifier = "AbsDownloaderPlugin"
public var jsName = "AbsDownloader"
public let pluginMethods: [CAPPluginMethod] = [
CAPPluginMethod(name: "downloadLibraryItem", returnType: CAPPluginReturnPromise)
]
static private let downloadsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
-20
View File
@@ -1,20 +0,0 @@
//
// AbsFileSystem.m
// App
//
// Created by advplyr on 5/13/22.
//
#import <Foundation/Foundation.h>
#import <Capacitor/Capacitor.h>
CAP_PLUGIN(AbsFileSystem, "AbsFileSystem",
CAP_PLUGIN_METHOD(selectFolder, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(checkFolderPermission, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(scanFolder, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(removeFolder, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(removeLocalLibraryItem, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(scanLocalLibraryItem, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(deleteItem, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(deleteTrackFromItem, CAPPluginReturnPromise);
)
+14 -1
View File
@@ -9,7 +9,20 @@ import Foundation
import Capacitor
@objc(AbsFileSystem)
public class AbsFileSystem: CAPPlugin {
public class AbsFileSystem: CAPPlugin, CAPBridgedPlugin {
public var identifier = "AbsFileSystemPlugin"
public var jsName = "AbsFileSystem"
public let pluginMethods: [CAPPluginMethod] = [
CAPPluginMethod(name: "selectFolder", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "checkFolderPermission", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "scanFolder", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "removeFolder", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "removeLocalLibraryItem", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "scanLocalLibraryItem", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "deleteItem", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "deleteTrackFromItem", returnType: CAPPluginReturnPromise)
]
private let logger = AppLogger(category: "AbsFileSystem")
@objc func selectFolder(_ call: CAPPluginCall) {
+47
View File
@@ -0,0 +1,47 @@
//
// AbsLogger.swift
// Audiobookshelf
//
// Created by advplyr on 4/20/25.
//
import Foundation
import Capacitor
@objc(AbsLogger)
public class AbsLogger: CAPPlugin, CAPBridgedPlugin {
public var identifier = "AbsLoggerPlugin"
public var jsName = "AbsLogger"
public let pluginMethods: [CAPPluginMethod] = [
CAPPluginMethod(name: "info", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "error", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "getAllLogs", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "clearLogs", returnType: CAPPluginReturnPromise)
]
private let logger = AppLogger(category: "AbsLogger")
@objc func info(_ call: CAPPluginCall) {
let message = call.getString("message") ?? ""
let tag = call.getString("tag") ?? ""
logger.log("[\(tag)] \(message)")
call.resolve()
}
@objc func error(_ call: CAPPluginCall) {
let message = call.getString("message") ?? ""
let tag = call.getString("tag") ?? ""
logger.error("[\(tag)] \(message)")
call.resolve()
}
@objc func getAllLogs(_ call: CAPPluginCall) {
call.unimplemented("Not implemented on iOS")
}
@objc func clearLogs(_ call: CAPPluginCall) {
call.unimplemented("Not implemented on iOS")
}
}
+10 -10
View File
@@ -1,5 +1,5 @@
PODS:
- Alamofire (5.8.1)
- Alamofire (5.10.2)
- Capacitor (7.2.0):
- CapacitorCordova
- CapacitorApp (7.0.1):
@@ -25,11 +25,11 @@ PODS:
- Capacitor
- CordovaPlugins (6.2.1):
- CapacitorCordova
- Realm (10.47.0):
- Realm/Headers (= 10.47.0)
- Realm/Headers (10.47.0)
- RealmSwift (10.47.0):
- Realm (= 10.47.0)
- Realm (10.54.4):
- Realm/Headers (= 10.54.4)
- Realm/Headers (10.54.4)
- RealmSwift (10.54.4):
- Realm (= 10.54.4)
- WebnativellcCapacitorFilesharer (7.0.4):
- Capacitor
@@ -88,7 +88,7 @@ EXTERNAL SOURCES:
:path: "../../node_modules/@webnativellc/capacitor-filesharer"
SPEC CHECKSUMS:
Alamofire: 3ca42e259043ee0dc5c0cdd76c4bc568b8e42af7
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
Capacitor: 106e7a4205f4618d582b886a975657c61179138d
CapacitorApp: d63334c052278caf5d81585d80b21905c6f93f39
CapacitorBrowser: 081852cf532acf77b9d2953f3a88fe5b9711fb06
@@ -102,10 +102,10 @@ SPEC CHECKSUMS:
CapacitorPreferences: cbf154e5e5519b7f5ab33817a334dda1e98387f9
CapacitorStatusBar: 275cbf2f4dfc00388f519ef80c7ec22edda342c9
CordovaPlugins: 5a72a85b45469e68556bb172409f1b6d57b27236
Realm: e43fb540ae947497e3ea8a662443256920602060
RealmSwift: 8b06ed06b5d16749ae0c4d91c0cba414a9e28189
Realm: 8b5cda39a41f17a1734da2f39c6004eb8745587a
RealmSwift: 0b4f808fed6898f1f6c26f501f740efd80dff0b4
WebnativellcCapacitorFilesharer: 10b111373d4dc49608935600dcbcc14605258c73
PODFILE CHECKSUM: 498821c0cfa2508609567fa95d7244c01cbef538
COCOAPODS: 1.12.1
COCOAPODS: 1.16.2
+21 -12
View File
@@ -16,6 +16,7 @@
<script>
import { CapacitorHttp } from '@capacitor/core'
import { AbsLogger } from '@/plugins/capacitor'
export default {
data() {
@@ -72,6 +73,9 @@ export default {
currentLibraryId() {
return this.$store.state.libraries.currentLibraryId
},
currentLibraryName() {
return this.$store.getters['libraries/getCurrentLibraryName']
},
attemptingConnection: {
get() {
return this.$store.state.attemptingConnection
@@ -114,6 +118,7 @@ export default {
console.warn('[default] attemptConnection')
if (!this.networkConnected) {
console.warn('[default] No network connection')
AbsLogger.info({ tag: 'default', message: 'attemptConnection: No network connection' })
return
}
if (this.attemptingConnection) {
@@ -134,13 +139,14 @@ export default {
if (!serverConfig) {
// No last server config set
this.attemptingConnection = false
AbsLogger.info({ tag: 'default', message: 'attemptConnection: No last server config set' })
return
}
console.log(`[default] Got server config, attempt authorize ${serverConfig.address}`)
AbsLogger.info({ tag: 'default', message: `attemptConnection: Got server config, attempt authorize (${serverConfig.name})` })
const authRes = await this.postRequest(`${serverConfig.address}/api/authorize`, null, { Authorization: `Bearer ${serverConfig.token}` }, 6000).catch((error) => {
console.error('[default] Server auth failed', error)
AbsLogger.error({ tag: 'default', message: `attemptConnection: Server auth failed (${serverConfig.name})` })
return false
})
if (!authRes) {
@@ -166,7 +172,7 @@ export default {
this.$socket.connect(serverConnectionConfig.address, serverConnectionConfig.token)
console.log('[default] Successful connection on last saved connection config', JSON.stringify(serverConnectionConfig))
AbsLogger.info({ tag: 'default', message: `attemptConnection: Successful connection to last saved server config (${serverConnectionConfig.name})` })
await this.initLibraries()
this.attemptingConnection = false
},
@@ -186,7 +192,8 @@ export default {
}
this.inittingLibraries = true
await this.$store.dispatch('libraries/load')
console.log(`[default] initLibraries loaded ${this.currentLibraryId}`)
AbsLogger.info({ tag: 'default', message: `initLibraries loading library ${this.currentLibraryName}` })
await this.$store.dispatch('libraries/fetch', this.currentLibraryId)
this.$eventBus.$emit('library-changed')
this.inittingLibraries = false
@@ -197,7 +204,7 @@ export default {
return
}
console.log('[default] Calling syncLocalSessions')
AbsLogger.info({ tag: 'default', message: 'Calling syncLocalSessions' })
const response = await this.$db.syncLocalSessionsWithServer(isFirstSync)
if (response?.error) {
console.error('[default] Failed to sync local sessions', response.error)
@@ -214,12 +221,12 @@ export default {
},
async userMediaProgressUpdated(payload) {
const prog = payload.data // MediaProgress
console.log(`[default] userMediaProgressUpdate checking for local media progress ${payload.id}`)
await AbsLogger.info({ tag: 'default', message: `userMediaProgressUpdate: Received updated media progress for current user from socket event. Media item id ${payload.id}` })
// Check if this media item is currently open in the player, paused, and this progress update is coming from a different session
const isMediaOpenInPlayer = this.$store.getters['getIsMediaStreaming'](prog.libraryItemId, prog.episodeId)
if (isMediaOpenInPlayer && this.$store.getters['getCurrentPlaybackSessionId'] !== payload.sessionId && !this.$store.state.playerIsPlaying) {
console.log('[default] userMediaProgressUpdated for current open media item', payload.data.currentTime)
await AbsLogger.info({ tag: 'default', message: `userMediaProgressUpdate: Item is currently open in player, paused and this progress update is coming from a different session. Updating playback time to ${payload.data.currentTime}` })
this.$eventBus.$emit('playback-time-update', payload.data.currentTime)
}
@@ -230,12 +237,12 @@ export default {
// Progress update is more recent then local progress
if (localProg && localProg.lastUpdate < prog.lastUpdate) {
if (localProg.currentTime == prog.currentTime && localProg.isFinished == prog.isFinished) {
console.log('[default] syncing progress server lastUpdate > local lastUpdate but currentTime and isFinished is equal')
await AbsLogger.info({ tag: 'default', message: `userMediaProgressUpdate: server lastUpdate is more recent but progress is up-to-date (libraryItemId: ${prog.libraryItemId}${prog.episodeId ? ` episodeId: ${prog.episodeId}` : ''})` })
return
}
// Server progress is more up-to-date
console.log(`[default] syncing progress from server with local item for "${prog.libraryItemId}" ${prog.episodeId ? `episode ${prog.episodeId}` : ''} | server lastUpdate=${prog.lastUpdate} > local lastUpdate=${localProg.lastUpdate}`)
await AbsLogger.info({ tag: 'default', message: `userMediaProgressUpdate: syncing progress from server with local item for "${prog.libraryItemId}" ${prog.episodeId ? `episode ${prog.episodeId}` : ''} | server lastUpdate=${prog.lastUpdate} > local lastUpdate=${localProg.lastUpdate}` })
const payload = {
localMediaProgressId: localProg.id,
mediaProgress: prog
@@ -273,7 +280,7 @@ export default {
}
if (newLocalMediaProgress?.id) {
console.log(`[default] local media progress updated for ${newLocalMediaProgress.id}`)
await AbsLogger.info({ tag: 'default', message: `userMediaProgressUpdate: local media progress updated for ${newLocalMediaProgress.id}` })
this.$store.commit('globals/updateLocalMediaProgress', newLocalMediaProgress)
}
},
@@ -310,6 +317,7 @@ export default {
this.$socket.on('user_media_progress_updated', this.userMediaProgressUpdated)
if (this.$store.state.isFirstLoad) {
AbsLogger.info({ tag: 'default', message: `mounted: initializing first load (${this.$platform} v${this.$config.version})` })
this.$store.commit('setIsFirstLoad', false)
this.loadSavedSettings()
@@ -322,17 +330,18 @@ export default {
await this.$store.dispatch('setupNetworkListener')
if (this.$store.state.user.serverConnectionConfig) {
AbsLogger.info({ tag: 'default', message: `mounted: Server connected, init libraries (${this.$store.getters['user/getServerConfigName']})` })
await this.initLibraries()
} else {
AbsLogger.info({ tag: 'default', message: `mounted: Server not connected, attempt connection` })
await this.attemptConnection()
}
console.log(`[default] finished connection attempt or already connected ${!!this.user}`)
await this.syncLocalSessions(true)
this.hasMounted = true
console.log('[default] fully initialized')
AbsLogger.info({ tag: 'default', message: 'mounted: fully initialized' })
this.$eventBus.$emit('abs-ui-ready')
}
},
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "audiobookshelf-app",
"version": "0.9.79-beta",
"version": "0.9.81-beta",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "audiobookshelf-app",
"version": "0.9.79-beta",
"version": "0.9.81-beta",
"dependencies": {
"@capacitor-community/keep-awake": "^7.0.0",
"@capacitor-community/volume-buttons": "^7.0.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "audiobookshelf-app",
"version": "0.9.79-beta",
"version": "0.9.81-beta",
"author": "advplyr",
"scripts": {
"dev": "nuxt --hostname 0.0.0.0 --port 1337",
+175
View File
@@ -0,0 +1,175 @@
<template>
<div class="w-full h-full py-4">
<div class="flex items-center mb-2 space-x-2 px-4">
<p class="text-lg font-bold">{{ $strings.ButtonLogs }}</p>
<ui-icon-btn outlined borderless :icon="isCopied ? 'check' : 'content_copy'" @click="copyToClipboard" />
<ui-icon-btn outlined borderless icon="share" @click="shareLogs" />
<div class="flex-grow"></div>
<ui-icon-btn outlined borderless icon="more_vert" @click="showDialog = true" />
</div>
<div class="w-full h-[calc(100%-40px)] overflow-y-auto relative" ref="logContainer">
<div v-if="!logs.length && !isLoading" class="flex items-center justify-center h-32 p-4">
<p class="text-gray-400">{{ $strings.MessageNoLogs }}</p>
</div>
<div v-if="hasScrolled" class="sticky top-0 left-0 w-full h-10 bg-gradient-to-t from-transparent to-bg z-10 pointer-events-none"></div>
<div v-for="(log, index) in logs" :key="log.id" class="py-2 px-4" :class="{ 'bg-white/5': index % 2 === 0 }">
<div class="flex items-center space-x-4 mb-1">
<div class="text-xs uppercase font-bold" :class="{ 'text-error': log.level === 'error', 'text-blue-500': log.level === 'info' }">{{ log.level }}</div>
<div class="text-xs text-gray-400">{{ formatEpochToDatetimeString(log.timestamp) }}</div>
<div class="flex-grow"></div>
<div class="text-xs text-gray-400">{{ log.tag }}</div>
</div>
<div class="text-xs">{{ maskServerAddress ? log.maskedMessage : log.message }}</div>
</div>
</div>
<modals-dialog v-model="showDialog" :items="dialogItems" @action="dialogAction" />
</div>
</template>
<script>
import { AbsLogger } from '@/plugins/capacitor'
import { FileSharer } from '@webnativellc/capacitor-filesharer'
export default {
data() {
return {
logs: [],
isLoading: true,
isCopied: false,
hasScrolled: false,
maskServerAddress: true,
showDialog: false
}
},
computed: {
dialogItems() {
return [
{
text: this.maskServerAddress ? this.$strings.ButtonUnmaskServerAddress : this.$strings.ButtonMaskServerAddress,
value: 'toggle-mask-server-address',
icon: this.maskServerAddress ? 'remove_moderator' : 'shield'
},
{
text: this.$strings.ButtonClearLogs,
value: 'clear-logs',
icon: 'delete'
}
]
}
},
methods: {
async dialogAction(action) {
await this.$hapticsImpact()
if (action === 'clear-logs') {
await AbsLogger.clearLogs()
this.logs = []
} else if (action === 'toggle-mask-server-address') {
this.maskServerAddress = !this.maskServerAddress
}
this.showDialog = false
},
toggleMaskServerAddress() {
this.maskServerAddress = !this.maskServerAddress
},
async copyToClipboard() {
await this.$hapticsImpact()
this.$copyToClipboard(this.getLogsString()).then(() => {
this.isCopied = true
setTimeout(() => {
this.isCopied = false
}, 2000)
})
},
/**
* Formats an epoch timestamp to YYYY-MM-DD HH:mm:ss.SSS
* Use 24 hour time format
* @param {number} epoch
* @returns {string}
*/
formatEpochToDatetimeString(epoch) {
return new Date(epoch)
.toLocaleString('en-US', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
fractionalSecondDigits: 3,
hour12: false
})
.replace(',', '')
},
getLogsString() {
return this.logs
.map((log) => {
const logMessage = this.maskServerAddress ? log.maskedMessage : log.message
return `${this.formatEpochToDatetimeString(log.timestamp)} [${log.level.toUpperCase()}] ${logMessage}`
})
.join('\n')
},
async shareLogs() {
await this.$hapticsImpact()
// Share .txt file with logs
const base64Data = Buffer.from(this.getLogsString()).toString('base64')
FileSharer.share({
filename: `abs_logs_${this.$platform}_${this.$config.version}.txt`,
contentType: 'text/plain',
base64Data
}).catch((error) => {
if (error.message !== 'USER_CANCELLED') {
console.error('Failed to share', error.message)
this.$toast.error('Failed to share: ' + error.message)
}
})
},
scrollToBottom() {
this.$refs.logContainer.scrollTop = this.$refs.logContainer.scrollHeight
this.hasScrolled = this.$refs.logContainer.scrollTop > 0
},
maskLogMessage(message) {
return message.replace(/(https?:\/\/)\S+/g, '$1[SERVER_ADDRESS]')
},
loadLogs() {
this.isLoading = true
AbsLogger.getAllLogs()
.then((logData) => {
const logs = logData.value || []
this.logs = logs.map((log) => {
log.maskedMessage = this.maskLogMessage(log.message)
return log
})
this.$nextTick(() => {
this.scrollToBottom()
})
this.isLoading = false
})
.catch((error) => {
this.isLoading = false
console.error('Failed to load logs', error)
this.$toast.error('Failed to load logs: ' + error.message)
})
}
},
mounted() {
AbsLogger.addListener('onLog', (log) => {
log.maskedMessage = this.maskLogMessage(log.message)
this.logs.push(log)
this.logs.sort((a, b) => a.timestamp - b.timestamp)
this.$nextTick(() => {
this.scrollToBottom()
})
})
this.loadLogs()
},
beforeDestroy() {
AbsLogger.removeAllListeners()
}
}
</script>
+17
View File
@@ -100,6 +100,13 @@
<p class="pl-4">{{ $strings.LabelDisableVibrateOnReset }}</p>
<span class="material-symbols text-xl ml-2" @click.stop="showInfo('disableSleepTimerResetFeedback')">info</span>
</div>
<div class="flex items-center py-3">
<div class="w-10 flex justify-center" @click="toggleSleepTimerAlmostDoneChime">
<ui-toggle-switch v-model="settings.enableSleepTimerAlmostDoneChime" @input="saveSettings" />
</div>
<p class="pl-4">{{ $strings.LabelSleepTimerAlmostDoneChime }}</p>
<span class="material-symbols text-xl ml-2" @click.stop="showInfo('enableSleepTimerAlmostDoneChime')">info</span>
</div>
<div class="flex items-center py-3">
<div class="w-10 flex justify-center" @click="toggleAutoSleepTimer">
<ui-toggle-switch v-model="settings.autoSleepTimer" @input="saveSettings" />
@@ -207,6 +214,7 @@ export default {
sleepTimerLength: 900000, // 15 minutes
disableSleepTimerFadeOut: false,
disableSleepTimerResetFeedback: false,
enableSleepTimerAlmostDoneChime: false,
autoSleepTimerAutoRewind: false,
autoSleepTimerAutoRewindTime: 300000, // 5 minutes
languageCode: 'en-us',
@@ -234,6 +242,10 @@ export default {
name: this.$strings.LabelDisableVibrateOnReset,
message: this.$strings.LabelDisableVibrateOnResetHelp
},
enableSleepTimerAlmostDoneChime: {
name: this.$strings.LabelSleepTimerAlmostDoneChime,
message: this.$strings.LabelSleepTimerAlmostDoneChimeHelp
},
autoSleepTimerAutoRewind: {
name: this.$strings.LabelAutoSleepTimerAutoRewind,
message: this.$strings.LabelAutoSleepTimerAutoRewindHelp
@@ -549,6 +561,10 @@ export default {
this.settings.disableSleepTimerResetFeedback = !this.settings.disableSleepTimerResetFeedback
this.saveSettings()
},
toggleSleepTimerAlmostDoneChime() {
this.settings.enableSleepTimerAlmostDoneChime = !this.settings.enableSleepTimerAlmostDoneChime
this.saveSettings()
},
toggleDisableAutoRewind() {
this.settings.disableAutoRewind = !this.settings.disableAutoRewind
this.saveSettings()
@@ -620,6 +636,7 @@ export default {
this.settings.sleepTimerLength = !isNaN(deviceSettings.sleepTimerLength) ? deviceSettings.sleepTimerLength : 900000 // 15 minutes
this.settings.disableSleepTimerFadeOut = !!deviceSettings.disableSleepTimerFadeOut
this.settings.disableSleepTimerResetFeedback = !!deviceSettings.disableSleepTimerResetFeedback
this.settings.enableSleepTimerAlmostDoneChime = !!deviceSettings.enableSleepTimerAlmostDoneChime
this.settings.autoSleepTimerAutoRewind = !!deviceSettings.autoSleepTimerAutoRewind
this.settings.autoSleepTimerAutoRewindTime = !isNaN(deviceSettings.autoSleepTimerAutoRewindTime) ? deviceSettings.autoSleepTimerAutoRewindTime : 300000 // 5 minutes
+4
View File
@@ -1,4 +1,5 @@
import { registerPlugin, WebPlugin } from '@capacitor/core'
import { AbsLogger } from '@/plugins/capacitor'
import { nanoid } from 'nanoid'
const { PlayerState } = require('../constants')
@@ -88,6 +89,9 @@ class AbsAudioPlayerWeb extends WebPlugin {
return
}
// For testing onLog events in web while on the logs page
AbsLogger.info({ tag: 'AbsAudioPlayer', message: 'playPause' })
if (this.player.paused) this.player.play()
else this.player.pause()
return {
+55
View File
@@ -0,0 +1,55 @@
import { registerPlugin, WebPlugin } from '@capacitor/core'
class AbsLoggerWeb extends WebPlugin {
constructor() {
super()
this.logs = []
}
saveLog(level, tag, message) {
const log = {
id: Math.random().toString(36).substring(2, 15),
tag: tag,
timestamp: Date.now(),
level: level,
message: message
}
this.logs.push(log)
this.notifyListeners('onLog', log)
}
// PluginMethod
async info(data) {
if (data?.message) {
this.saveLog('info', data.tag || '', data.message)
console.log('AbsLogger: info', `[${data.tag || ''}]:`, data.message)
}
}
// PluginMethod
async error(data) {
if (data?.message) {
this.saveLog('error', data.tag || '', data.message)
console.error('AbsLogger: error', `[${data.tag || ''}]:`, data.message)
}
}
// PluginMethod
async getAllLogs() {
return {
value: this.logs
}
}
// PluginMethod
async clearLogs() {
this.logs = []
}
}
const AbsLogger = registerPlugin('AbsLogger', {
web: () => new AbsLoggerWeb()
})
export { AbsLogger }
+2 -5
View File
@@ -2,12 +2,9 @@ import Vue from 'vue'
import { AbsAudioPlayer } from './AbsAudioPlayer'
import { AbsDownloader } from './AbsDownloader'
import { AbsFileSystem } from './AbsFileSystem'
import { AbsLogger } from './AbsLogger'
import { Capacitor } from '@capacitor/core'
Vue.prototype.$platform = Capacitor.getPlatform()
export {
AbsAudioPlayer,
AbsDownloader,
AbsFileSystem
}
export { AbsAudioPlayer, AbsDownloader, AbsFileSystem, AbsLogger }
+126 -10
View File
@@ -20,24 +20,26 @@
"ButtonDisableAutoTimer": "تعطيل المؤقت التلقائي",
"ButtonDisconnect": "فصل",
"ButtonGoToWebClient": "اذهب إلى عميل الويب",
"ButtonHistory": "تاريخ",
"ButtonHistory": "السجل التاريخي",
"ButtonHome": "الرئيسية",
"ButtonIssues": "مشاكل",
"ButtonLatest": "أحدث",
"ButtonLatest": "الأحدث",
"ButtonLibrary": "المكتبة",
"ButtonLocalMedia": "ملفات الوسائط المحلية",
"ButtonLocalMedia": "الوسائط المحلية",
"ButtonLogs": "سجلات",
"ButtonManageLocalFiles": "إدارة الملفات المحلية",
"ButtonNewFolder": "مجلد جديد",
"ButtonNextEpisode": "الحلقة التالية",
"ButtonOk": "موافق",
"ButtonOpenFeed": "فتح التغذية",
"ButtonOverride": "تجاوز",
"ButtonPause": "تَوَقَّف",
"ButtonPause": "إيقاف مؤقت",
"ButtonPlay": "تشغيل",
"ButtonPlayEpisode": "شغل الحلقة",
"ButtonPlayEpisode": "تشغيل الحلقة",
"ButtonPlaylists": "قوائم التشغيل",
"ButtonRead": "اقرأ",
"ButtonReadLess": "قلص",
"ButtonReadMore": "المزيد",
"ButtonReadLess": "اقرأ أقل",
"ButtonReadMore": "اقرأ أكثر",
"ButtonRemove": "إزالة",
"ButtonRemoveFromServer": "إزالة من الخادم",
"ButtonSave": "حفظ",
@@ -47,21 +49,135 @@
"ButtonSeries": "سلسلة",
"ButtonSetTimer": "اضبط مؤقت",
"ButtonStream": "بث",
"ButtonSubmit": "تقديم",
"ButtonSubmit": "إرسال",
"ButtonSwitchServerUser": "تبديل الخادم/المستخدم",
"ButtonUserStats": "إحصائيات المستخدم",
"ButtonYes": "نعم",
"HeaderAccount": "الحساب",
"HeaderAdvanced": "متقدم",
"HeaderAudioTracks": "المسارات الصوتية",
"HeaderAndroidAutoSettings": "إعدادات أندرويد للسيارة",
"HeaderAudioTracks": "المقاطع الصوتية",
"HeaderChapters": "الفصول",
"HeaderCollection": "مجموعة",
"HeaderCollectionItems": "عناصر المجموعة",
"HeaderConnectionStatus": "حالة الاتصال",
"HeaderDataSettings": "إعدادات البيانات",
"HeaderDetails": "التفاصيل",
"HeaderDownloads": "التنزيلات",
"HeaderEbookFiles": "ملفات الكتب الإلكترونية",
"HeaderEpisodes": "الحلقات",
"HeaderEreaderSettings": "إعدادات القارئ الإلكتروني",
"HeaderLatestEpisodes": "أحدث الحلقات",
"HeaderLibraries": "المكتبات"
"HeaderLibraries": "المكتبات",
"HeaderLocalFolders": "المجلدات المحلية",
"HeaderLocalLibraryItems": "عناصر المكتبة المحلية",
"HeaderNewPlaylist": "قائمة تشغيل جديدة",
"HeaderOpenRSSFeed": "فتح تغذية RSS",
"HeaderPlaybackSettings": "إعدادات التشغيل",
"HeaderPlaylist": "قائمة تشغيل",
"HeaderPlaylistItems": "عناصر قائمة التشغيل",
"HeaderProgressSyncFailed": "فشلت المزامنة",
"HeaderRSSFeed": "تغذية RSS",
"HeaderRSSFeedGeneral": "تفاصيل RSS",
"HeaderRSSFeedIsOpen": "مغذي RSS مفتوح",
"HeaderSelectDownloadLocation": "حدد موقع التنزيل",
"HeaderSettings": "إعدادات",
"HeaderSleepTimer": "مؤقت النوم",
"HeaderSleepTimerSettings": "إعدادات مؤقت النوم",
"HeaderStatsMinutesListeningChart": "الدقائق المسموعة (آخر 7 أيام)",
"HeaderStatsRecentSessions": "الجلسات الأخيرة",
"HeaderTableOfContents": "جدول المحتويات",
"HeaderUserInterfaceSettings": "إعدادات واجهة المستخدم",
"HeaderYourStats": "إحصائياتك",
"LabelAddToPlaylist": "أضف إلى قائمة التشغيل",
"LabelAddedAt": "أضيفت على",
"LabelAddedDate": "تمت الإضافة",
"LabelAll": "الكل",
"LabelAllowSeekingOnMediaControls": "السماح بالبحث عن الموضع في التحكم بإشعارات الوسائط",
"LabelAlways": "دائماً",
"LabelAndroidAutoBrowseLimitForGrouping": "حد السحب الأبجدي",
"LabelAndroidAutoBrowseLimitForGroupingHelp": "لا تستخدم السحب الأبجدي عندما يكون عدد العناصر المراد عرضها أقل من هذا العدد",
"LabelAndroidAutoBrowseSeriesSequenceOrder": "ترتيب كتب السلسلة",
"LabelAskConfirmation": "اطلب التأكيد",
"LabelAuthor": "المؤلف",
"LabelAuthorFirstLast": "المؤلف (الاسم الأول الأخير)",
"LabelAuthorLastFirst": "المؤلف (الاسم الأخير، الأول)",
"LabelAuthors": "المؤلفون",
"LabelAutoDownloadEpisodes": "تنزيل الحلقات تلقائيًا",
"LabelAutoRewindTime": "إعادة الوقت تلقائياَ",
"LabelAutoSleepTimer": "مؤقت النوم التلقائي",
"LabelAutoSleepTimerAutoRewind": "مؤقتا النوم والإرجاع التلقائيين",
"LabelAutoSleepTimerAutoRewindHelp": "عندما ينتهي مؤقت النوم التلقائي، فإن تشغيل العنصر مرة أخرى سيؤدي إلى الإرجاع التلقائي.",
"LabelAutoSleepTimerHelp": "عند تشغيل الوسائط بين أوقات البداية والنهاية المحددة، سيبدأ مؤقت النوم تلقائيًا.",
"LabelBooks": "الكتب",
"LabelChapterTrack": "مسار الفصل",
"LabelChapters": "الفصول",
"LabelClosePlayer": "إغلاق المشغل",
"LabelCollapseSeries": "إخفاء المسلسلات",
"LabelComplete": "مكتمل",
"LabelContinueBooks": "استمرار الكتب",
"LabelContinueEpisodes": "استمرار الحلقات",
"LabelContinueListening": "استمرار الاستماع",
"LabelContinueReading": "استمرار القراءة",
"LabelContinueSeries": "استمرار المسلسلات",
"LabelCustomTime": "الوقت المخصص",
"LabelDescription": "الوصف",
"LabelDisableAudioFadeOut": "تعطيل التلاشي الصوتي",
"LabelDisableAudioFadeOutHelp": "سيبدأ مستوى الصوت بالانخفاض عندما يتبقى أقل من دقيقة واحدة على مؤقت النوم. فعّل هذا الإعداد لعدم التلاشي.",
"LabelDisableAutoRewind": "تعطيل الإعادة التلقائية",
"LabelDisableShakeToReset": "تعطيل الرج لإعادة الضبط",
"LabelDisableShakeToResetHelp": "سيؤدي هزّ جهازك أثناء تشغيل المؤقت أو خلال دقيقتين من انتهاءه إلى إعادة ضبط مؤقت النوم. فعّل هذا الإعداد لتعطيل الرج لإعادة الضبط.",
"LabelDisableVibrateOnReset": "تعطيل الاهتزاز عند إعادة الضبط",
"LabelDisableVibrateOnResetHelp": "عند إعادة ضبط مؤقت النوم، سيهتز جهازك. فعّل هذا الإعداد لعدم الاهتزاز عند إعادة ضبط مؤقت النوم.",
"LabelDiscover": "استكشف",
"LabelDownload": "تنزيل",
"LabelDownloadUsingCellular": "التنزيل عبر بيانات الهاتف",
"LabelDownloaded": "تم تنزيلها",
"LabelDuration": "المدة",
"LabelEbook": "الكتاب الإلكتروني",
"LabelEbooks": "الكتب الإلكترونية",
"LabelEnable": "تمكين",
"LabelEnableMp3IndexSeeking": "تمكين البحث عن فهرس mp3",
"LabelEnableMp3IndexSeekingHelp": "يجب تفعيل هذا الإعداد فقط إذا كانت ملفات MP3 لديك لا تعمل بشكل صحيح. غالبًا ما يكون البحث غير الدقيق ناتجًا عن ملفات MP3 ذات معدل البت المتغير (VBR). سيفرض هذا الإعداد البحث عن الفهرس، حيث يتم إنشاء تعيين زمني للبايت أثناء قراءة الملف. في بعض الحالات، مع ملفات MP3 كبيرة الحجم، قد يكون هناك تأخير عند البحث قرب نهاية الملف.",
"LabelEnd": "انهاء",
"LabelEndOfChapter": "نهاية الفصل",
"LabelEndTime": "وقت النهاية",
"LabelEpisode": "الحلقة",
"LabelFeedURL": "عنوان التغذية",
"LabelFile": "الملف",
"LabelFileBirthtime": "وقت انشاء الملف",
"LabelFileModified": "تم تعديل الملف",
"LabelFilename": "اسم الملف",
"LabelFinished": "المنجزة",
"LabelFolder": "المجلد",
"LabelFontBoldness": "تعريض الخط",
"LabelFontScale": "نطاق الخط",
"LabelGenre": "التصنيف",
"LabelGenres": "التصانيف",
"LabelHapticFeedback": "ردود الفعل اللمسية",
"LabelHasEbook": "يحتوي كتاب إلكتروني",
"LabelHasSupplementaryEbook": "يحتوي كتاب إلكتروني تكميلي",
"LabelHeavy": "ثقيل",
"LabelHigh": "مرتفع",
"LabelHost": "المضيف",
"LabelInProgress": "تحت التنفيذ",
"LabelIncomplete": "غير مكتمل",
"LabelInternalAppStorage": "وحدة تخزين التطبيق الداخلي",
"LabelJumpBackwardsTime": "وقت القفز للخلف",
"LabelJumpForwardsTime": "وقت القفز للأمام",
"LabelKeepScreenAwake": "إبقاء الشاشة يقظة",
"LabelLanguage": "اللغة",
"LabelLayout": "التنسيق",
"LabelLayoutAuto": "تلقائي",
"LabelLayoutSinglePage": "صفحة واحدة",
"LabelLight": "خفيف/فاتح",
"LabelLineSpacing": "تباعد الأسطر",
"LabelListenAgain": "الاستماع مجدداً",
"LabelLocalBooks": "الكتب المحلية",
"LabelLocalPodcasts": "المدونات الصوتية المحلية",
"LabelLockOrientation": "قفل الاتجاه",
"LabelLockPlayer": "قفل المشغل",
"LabelLow": "منخفض",
"LabelMediaType": "نوع الوسائط",
"LabelMedium": "متوسط"
}
+20 -3
View File
@@ -26,6 +26,7 @@
"ButtonLatest": "Nejnovější",
"ButtonLibrary": "Knihovna",
"ButtonLocalMedia": "Místní média",
"ButtonLogs": "Záznamy",
"ButtonManageLocalFiles": "Spravovat místní soubory",
"ButtonNewFolder": "Nová složka",
"ButtonNextEpisode": "Další epizoda",
@@ -66,7 +67,7 @@
"HeaderEbookFiles": "Soubory e-knih",
"HeaderEpisodes": "Epizody",
"HeaderEreaderSettings": "Nastavení čtečky e-knih",
"HeaderLatestEpisodes": "Nejnovější epizody",
"HeaderLatestEpisodes": "Nové epizody",
"HeaderLibraries": "Knihovny",
"HeaderLocalFolders": "Místní složky",
"HeaderLocalLibraryItems": "Místní položky knihovny",
@@ -75,7 +76,7 @@
"HeaderPlaybackSettings": "Nastavení přehrávání",
"HeaderPlaylist": "Seznam skladeb",
"HeaderPlaylistItems": "Položky seznamu přehrávání",
"HeaderProgressSyncFailed": "Chyba při synchronizaci",
"HeaderProgressSyncFailed": "Chyba při synchronizaci pokroku",
"HeaderRSSFeed": "RSS kanál",
"HeaderRSSFeedGeneral": "Detaily RSS",
"HeaderRSSFeedIsOpen": "Kanál RSS je otevřen",
@@ -94,6 +95,9 @@
"LabelAll": "Vše",
"LabelAllowSeekingOnMediaControls": "Povolit vyhledávání polohy v ovládacích prvcích oznámení médií",
"LabelAlways": "Vždy",
"LabelAndroidAutoBrowseLimitForGrouping": "Omezení abecedního rozbalovacího seznamu",
"LabelAndroidAutoBrowseLimitForGroupingHelp": "Nepoužívejte abecední řazení, pokud je k zobrazení méně než tento počet položek",
"LabelAndroidAutoBrowseSeriesSequenceOrder": "Řazení sérií knih",
"LabelAskConfirmation": "Požádat o potvrzení",
"LabelAuthor": "Autor",
"LabelAuthorFirstLast": "Autor (jméno a příjmení)",
@@ -134,7 +138,7 @@
"LabelEbooks": "E-knihy",
"LabelEnable": "Povolit",
"LabelEnableMp3IndexSeeking": "Povolit indexové vyhledávání mp3",
"LabelEnableMp3IndexSeekingHelp": "Toto nastavení by mělo být povoleno pouze v případě, že soubory MP3 nejsou správně vyhledávány. Nepřesné vyhledávání je s největší pravděpodobností způsobeno soubory MP3 s proměnlivým datovým tokem (VBR). Toto nastavení vynutí indexové vyhledávání, při kterém se při čtení souboru vytváří mapování času na bajty. V některých případech u velkých souborů MP3 dochází ke zpoždění při vyhledávání ke konci souboru.",
"LabelEnableMp3IndexSeekingHelp": "Toto nastavení by mělo být povoleno pouze v případě, že v souborech MP3 nelze přeskakovat. Nepřesné přeskakování je s největší pravděpodobností způsobeno soubory MP3 s proměnlivým datovým tokem (VBR). Toto nastavení vynutí indexové vyhledávání, při kterém se při čtení souboru vytváří mapování času na bajty. V některých případech u velkých souborů MP3 dochází ke zpoždění při vyhledávání ke konci souboru.",
"LabelEnd": "Konec",
"LabelEndOfChapter": "Konec kapitoly",
"LabelEndTime": "Do",
@@ -161,6 +165,7 @@
"LabelInternalAppStorage": "Interní úložiště aplikace",
"LabelJumpBackwardsTime": "Délka skoku zpět v čase",
"LabelJumpForwardsTime": "Délka skoku vpřed v čase",
"LabelKeepScreenAwake": "Nezhasínat obrazovku",
"LabelLanguage": "Jazyk",
"LabelLayout": "Rozvržení",
"LabelLayoutAuto": "Automatické",
@@ -175,6 +180,7 @@
"LabelLow": "Nízké",
"LabelMediaType": "Typ média",
"LabelMedium": "Střední",
"LabelMissing": "Chybějící",
"LabelMore": "Více",
"LabelMoreInfo": "Více informací",
"LabelName": "Jméno",
@@ -192,6 +198,8 @@
"LabelNotFinished": "Nedokončeno",
"LabelNotStarted": "Nezahájeno",
"LabelNumEpisodes": "{0} epizod",
"LabelNumEpisodesIncomplete": "{0} epizod, {1} nekompletní",
"LabelNumberOfEpisodes": "Počet epizod",
"LabelOff": "Vypnout",
"LabelOn": "Zapnuto",
"LabelPassword": "Heslo",
@@ -220,6 +228,8 @@
"LabelScaleElapsedTimeBySpeed": "Škálovat uplynulý čas podle rychlosti",
"LabelSeason": "Sezóna",
"LabelSelectADevice": "Vyberte zařízení",
"LabelSequenceAscending": "Řadit vzestupně",
"LabelSequenceDescending": "Řadit sestupně",
"LabelSeries": "Série",
"LabelServerAddress": "Adresa serveru",
"LabelSetEbookAsPrimary": "Nastavit jako primární",
@@ -243,6 +253,7 @@
"LabelTag": "Štítek",
"LabelTags": "Štítky",
"LabelTheme": "Téma",
"LabelThemeBlack": "Černé",
"LabelThemeDark": "Tmavé",
"LabelThemeLight": "Světlé",
"LabelTimeRemaining": "{0} zbývá",
@@ -269,6 +280,7 @@
"MessageBookshelfEmpty": "Knihovna je prázdná",
"MessageConfirmDeleteLocalEpisode": "Odebrat místní epizodu „{0}“ ze zařízení? Soubor na serveru zůstane nezměněný.",
"MessageConfirmDeleteLocalFiles": "Odebrat místní soubory této položky ze zařízení? Soubory na serveru a váš pokrok nebudou ovlivněny.",
"MessageConfirmDisableAutoTimer": "Určitě chcete vypnout automatický časovač pro zbytek dnešního dne? Časovač bude opět aktivován po uběhnutí doby automatického spánku nebo po restartování aplikace.",
"MessageConfirmDiscardProgress": "Opravdu chcete zahodit svůj pokrok?",
"MessageConfirmDownloadUsingCellular": "Chystáte se stahovat přes mobilní data. Toto může zahrnovat poplatky za mobilní data. Chcete pokračovat?",
"MessageConfirmMarkAsFinished": "Opravdu chcete tuto položku označit jako dokončenou?",
@@ -283,8 +295,10 @@
"MessageFetching": "Načítání...",
"MessageFollowTheProjectOnGithub": "Sledujte projekt na GitHubu",
"MessageItemDownloadCompleteFailedToCreate": "Stahování položky bylo dokončeno, ale nepodařilo se vytvořit položku v knihovně",
"MessageItemMissing": "Položka chybí a musí být opravena na serveru. Typicky je položka označena jako chybějící, protože cesty k souborům nejsou přístupné.",
"MessageLoading": "Načítá se...",
"MessageLoadingServerData": "Načítání dat ze serveru...",
"MessageLocalFolderDescription": "„Interní úložiště aplikace“ je přístupné pouze této aplikaci. Tato aplikace podporuje pouze média stažená přímo prostřednictvím aplikace. Složky sdíleného úložiště lze použít k tomu, aby ostatní aplikace měly přístup k médiím staženým touto aplikací.",
"MessageMarkAsFinished": "Označit jako dokončené",
"MessageMediaLinkedToADifferentServer": "Média jsou propojena se serverem Audiobookshelf na jiné adrese ({0}). Pokrok bude synchronizován, když budete připojeni k této adrese serveru.",
"MessageMediaLinkedToADifferentUser": "Média jsou propojena se serverem, ale byla stažena jiným uživatelem. Pokrok bude synchronizován pouze s uživatelem, který je stáhl.",
@@ -303,7 +317,10 @@
"MessageNoSeries": "Žádné série",
"MessageNoUpdatesWereNecessary": "Nebyly nutné žádné aktualizace",
"MessageNoUserPlaylists": "Nemáte žádné seznamy skladeb",
"MessageOldServerConnectionWarning": "Konfigurace připojení k serveru používá staré ID uživatele. Odstraňte a znovu přidejte toto připojení k serveru.",
"MessageOldServerConnectionWarningHelp": "Připojení k tomuto serveru jste původně nastavili před migrací databáze ve verzi 2.3.0 vydané v červnu 2023. Budoucí aktualizace serveru odstraní možnost přihlášení pomocí tohoto starého připojení. Odstraňte prosím stávající připojení k serveru a připojte se znovu (pomocí stejné adresy serveru a přihlašovacích údajů). Pokud máte v tomto zařízení stažená média, bude nutné je pro synchronizaci se serverem stáhnout znovu.",
"MessagePodcastSearchField": "Zadejte hledaný pojem pro RSS feed URL",
"MessageProgressSyncFailed": "Poslední pokus o nahlášení průběhu poslechu na server se nezdařil. Během přehrávání médií se budou požadavky na synchronizaci pokroku nadále pokoušet každých 15 až 60 sekund.",
"MessageReportBugsAndContribute": "Nahlašte chyby, vyžádejte si funkce a přispěte na",
"MessageSeriesAlreadyDownloaded": "Všechny knihy v této sérii jste již stáhli.",
"MessageSeriesDownloadConfirm": "Stáhnout chybějící {0} knihu(y) s {1} souborem(y), celkem {2} do složky {3}?",
+5
View File
@@ -6,6 +6,7 @@
"ButtonCancel": "Abbrechen",
"ButtonCancelTimer": "Timer abbrechen",
"ButtonClearFilter": "Filter löschen",
"ButtonClearLogs": "Protokolle löschen",
"ButtonCloseFeed": "Feed schließen",
"ButtonCollections": "Sammlungen",
"ButtonConnect": "Verbinden",
@@ -26,7 +27,9 @@
"ButtonLatest": "Neueste",
"ButtonLibrary": "Bibliothek",
"ButtonLocalMedia": "Lokale Medien",
"ButtonLogs": "Protokolle",
"ButtonManageLocalFiles": "Verwalte lokale Dateien",
"ButtonMaskServerAddress": "Server Adresse verstecken",
"ButtonNewFolder": "Neuer Ordner",
"ButtonNextEpisode": "Nächste Episode",
"ButtonOk": "Einverstanden",
@@ -50,6 +53,7 @@
"ButtonStream": "Streamen",
"ButtonSubmit": "Ok",
"ButtonSwitchServerUser": "Wechsle Server/Benutzer",
"ButtonUnmaskServerAddress": "Server Adresse anzeigen",
"ButtonUserStats": "Nutzerstatistiken",
"ButtonYes": "Ja",
"HeaderAccount": "Konto",
@@ -310,6 +314,7 @@
"MessageNoItems": "Keine Medien",
"MessageNoItemsFound": "Keine Medien gefunden",
"MessageNoListeningSessions": "Keine Hörsitzungen",
"MessageNoLogs": "Keine Protokolle vorhanden",
"MessageNoMediaFolders": "Keine Medien Ordner",
"MessageNoNetworkConnection": "Keine Netzwerkverbindung",
"MessageNoPodcastsFound": "Keine Podcasts gefunden",
+7
View File
@@ -6,6 +6,7 @@
"ButtonCancel": "Cancel",
"ButtonCancelTimer": "Cancel Timer",
"ButtonClearFilter": "Clear Filter",
"ButtonClearLogs": "Clear Logs",
"ButtonCloseFeed": "Close Feed",
"ButtonCollections": "Collections",
"ButtonConnect": "Connect",
@@ -26,7 +27,9 @@
"ButtonLatest": "Latest",
"ButtonLibrary": "Library",
"ButtonLocalMedia": "Local Media",
"ButtonLogs": "Logs",
"ButtonManageLocalFiles": "Manage Local Files",
"ButtonMaskServerAddress": "Mask server address",
"ButtonNewFolder": "New Folder",
"ButtonNextEpisode": "Next Episode",
"ButtonOk": "Ok",
@@ -50,6 +53,7 @@
"ButtonStream": "Stream",
"ButtonSubmit": "Submit",
"ButtonSwitchServerUser": "Switch Server/User",
"ButtonUnmaskServerAddress": "Unmask server address",
"ButtonUserStats": "User Stats",
"ButtonYes": "Yes",
"HeaderAccount": "Account",
@@ -237,6 +241,8 @@
"LabelShowAll": "Show All",
"LabelSize": "Size",
"LabelSleepTimer": "Sleep timer",
"LabelSleepTimerAlmostDoneChime": "Play a chime when almost finished",
"LabelSleepTimerAlmostDoneChimeHelp": "Play a chime when the sleep timer has 30 seconds remaining",
"LabelStart": "Start",
"LabelStartTime": "Start time",
"LabelStatsBestDay": "Best Day",
@@ -310,6 +316,7 @@
"MessageNoItems": "No Items",
"MessageNoItemsFound": "No items found",
"MessageNoListeningSessions": "No Listening Sessions",
"MessageNoLogs": "No logs",
"MessageNoMediaFolders": "No Media Folders",
"MessageNoNetworkConnection": "No network connection",
"MessageNoPodcastsFound": "No podcasts found",
+4
View File
@@ -26,7 +26,9 @@
"ButtonLatest": "Najnovije",
"ButtonLibrary": "Knjižnica",
"ButtonLocalMedia": "Preuzeti medijski zapisi",
"ButtonLogs": "Zapisnici",
"ButtonManageLocalFiles": "Upravljanje preuzetim datotekama",
"ButtonMaskServerAddress": "Maskiraj adresu poslužitelja",
"ButtonNewFolder": "Nova mapa",
"ButtonNextEpisode": "Sljedeći nastavak",
"ButtonOk": "U redu",
@@ -50,6 +52,7 @@
"ButtonStream": "Reproduciraj strujanjem",
"ButtonSubmit": "Pošalji",
"ButtonSwitchServerUser": "Promijeni poslužitelj/korisnika",
"ButtonUnmaskServerAddress": "Prikaži adresu poslužitelja",
"ButtonUserStats": "Statistika korisnika",
"ButtonYes": "Da",
"HeaderAccount": "Korisnički račun",
@@ -310,6 +313,7 @@
"MessageNoItems": "Nema stavki",
"MessageNoItemsFound": "Nema pronađenih stavki",
"MessageNoListeningSessions": "Nema sesija slušanja",
"MessageNoLogs": "Nema zapisnika",
"MessageNoMediaFolders": "Nema medijskih mapa",
"MessageNoNetworkConnection": "Nema mrežne veze",
"MessageNoPodcastsFound": "Nije pronađen niti jedan podcast",
+11 -2
View File
@@ -6,6 +6,7 @@
"ButtonCancel": "Mégsem",
"ButtonCancelTimer": "Időzítő megszakítása",
"ButtonClearFilter": "Szűrő törlése",
"ButtonClearLogs": "Naplók tisztítása",
"ButtonCloseFeed": "Hírcsatorna bezárása",
"ButtonCollections": "Gyűjtemények",
"ButtonConnect": "Csatlakozás",
@@ -26,7 +27,9 @@
"ButtonLatest": "Legújabb",
"ButtonLibrary": "Könyvtár",
"ButtonLocalMedia": "Helyi média",
"ButtonLogs": "Naplók",
"ButtonManageLocalFiles": "Helyi fájlok kezelése",
"ButtonMaskServerAddress": "Kiszolgáló címének maszkolása",
"ButtonNewFolder": "Új mappa",
"ButtonNextEpisode": "Következő epizód",
"ButtonOk": "Ok",
@@ -50,6 +53,7 @@
"ButtonStream": "Streamelés",
"ButtonSubmit": "Küldés",
"ButtonSwitchServerUser": "Szerver/Felhasználó váltása",
"ButtonUnmaskServerAddress": "Kiszolgáló címének maszkolásának törlése",
"ButtonUserStats": "Felhasználói statisztikák",
"ButtonYes": "Igen",
"HeaderAccount": "Fiók",
@@ -95,6 +99,7 @@
"LabelAllowSeekingOnMediaControls": "Pozíció keresés engedélyezése a média értesítési vezérlőkön",
"LabelAlways": "Mindig",
"LabelAndroidAutoBrowseLimitForGrouping": "Ábécé szerinti lehívási korlát",
"LabelAndroidAutoBrowseLimitForGroupingHelp": "Ne használja az ábécé szerinti lehívási korlátot, ha ennél kevesebb elemet kell megjeleníteni",
"LabelAndroidAutoBrowseSeriesSequenceOrder": "A sorozat könyveinek sorrendje",
"LabelAskConfirmation": "Kérjen megerősítést",
"LabelAuthor": "Szerző",
@@ -136,7 +141,7 @@
"LabelEbooks": "E-könyvek",
"LabelEnable": "Engedélyezés",
"LabelEnableMp3IndexSeeking": "Mp3 indexkeresés engedélyezése",
"LabelEnableMp3IndexSeekingHelp": "Ezt a beállítást csak akkor kell engedélyezni, ha vannak mp3 fájljai, amelyek nem megfelelően keresnek. A pontatlan keresés leginkább a változó bitráta (VBR) mp3 fájlok miatt lehetséges. Ez a beállítás indexkeresést fog kényszeríteni, amely során egy idő-bájt leképezés jön létre, ahogy a fájlt olvassák. Nagy mp3 fájlok esetén előfordulhat késedelem a fájl végéhez való kereséskor.",
"LabelEnableMp3IndexSeekingHelp": "Ezt a beállítást csak akkor kell engedélyezni, ha vannak mp3 fájljai, amelyek nem megfelelően keresnek. A pontatlan keresés leginkább a változó bitráta (VBR) mp3 fájlok miatt lehetséges. Ez a beállítás indexkeresést fog kényszeríteni, amely során egy idő-bájt leképezés jön létre, ahogy a fájlt olvassák. Nagy mp3 fájlok esetén előfordulhat késedelem a fájl végénél történő kereséskor.",
"LabelEnd": "Vége",
"LabelEndOfChapter": "Fejezet vége",
"LabelEndTime": "Befejezés ideje",
@@ -178,6 +183,7 @@
"LabelLow": "Alacsony",
"LabelMediaType": "Média típus",
"LabelMedium": "Közepes",
"LabelMissing": "Hiányzik",
"LabelMore": "Több",
"LabelMoreInfo": "Több információ",
"LabelName": "Név",
@@ -240,7 +246,7 @@
"LabelStatsBestDay": "Legjobb nap",
"LabelStatsDailyAverage": "Napi átlag",
"LabelStatsDays": "Napok",
"LabelStatsDaysListened": "Hallgatott napok",
"LabelStatsDaysListened": "Hallgatással töltött napok",
"LabelStatsInARow": "egymás után",
"LabelStatsItemsFinished": "Befejezett elemek",
"LabelStatsMinutes": "perc",
@@ -250,6 +256,7 @@
"LabelTag": "Címke",
"LabelTags": "Címkék",
"LabelTheme": "Téma",
"LabelThemeBlack": "Fekete",
"LabelThemeDark": "Sötét",
"LabelThemeLight": "Világos",
"LabelTimeRemaining": "{0} maradt",
@@ -294,6 +301,7 @@
"MessageItemMissing": "Az elem hiányzik, és a szerveren kell javítani. Egy elemet általában azért jelölnek hiányzónak, mert a fájl elérési útvonalai nem elérhetőek.",
"MessageLoading": "Betöltés...",
"MessageLoadingServerData": "Szerveradatok betöltése...",
"MessageLocalFolderDescription": "A „Belső alkalmazás-tároló” csak ezzel az alkalmazással érhető el. Ez az alkalmazás csak a közvetlenül az alkalmazáson keresztül letöltött médiát támogatja. A megosztott tároló mappák segítségével más alkalmazások is hozzáférhetnek az alkalmazás által letöltött médiához.",
"MessageMarkAsFinished": "Befejezettnek jelölés",
"MessageMediaLinkedToADifferentServer": "A média egy másik címen található Audiobookshelf szerverhez kapcsolódik ({0}). A haladás szinkronizálva lesz, amikor csatlakozik ehhez a szervercímhez.",
"MessageMediaLinkedToADifferentUser": "A média ehhez a szerverhez kapcsolódik, de egy másik felhasználó töltötte le. A haladás csak a letöltést végrehajtó felhasználóhoz lesz szinkronizálva.",
@@ -306,6 +314,7 @@
"MessageNoItems": "Nincsenek elemek",
"MessageNoItemsFound": "Nincs találat",
"MessageNoListeningSessions": "Nincsenek hallgatási munkamenetek",
"MessageNoLogs": "Nincsenek naplók",
"MessageNoMediaFolders": "Nincsenek médiamappák",
"MessageNoNetworkConnection": "Nincs hálózati kapcsolat",
"MessageNoPodcastsFound": "Nem találhatók podcastok",
+12
View File
@@ -6,6 +6,7 @@
"ButtonCancel": "Annuleren",
"ButtonCancelTimer": "Timer annuleren",
"ButtonClearFilter": "Filter verwijderen",
"ButtonClearLogs": "Log bestanden verwijderen",
"ButtonCloseFeed": "Feed sluiten",
"ButtonCollections": "Collecties",
"ButtonConnect": "Verbinden",
@@ -26,9 +27,11 @@
"ButtonLatest": "Meest recent",
"ButtonLibrary": "Bibliotheek",
"ButtonLocalMedia": "Lokale Media",
"ButtonLogs": "Log bestanden",
"ButtonManageLocalFiles": "Beheer Lokale Bestanden",
"ButtonNewFolder": "Nieuwe map",
"ButtonNextEpisode": "Volgende aflevering",
"ButtonOk": "Akkoord",
"ButtonOpenFeed": "Feed openen",
"ButtonOverride": "Overschrijven",
"ButtonPause": "Pauze",
@@ -74,6 +77,7 @@
"HeaderPlaybackSettings": "Afspeel instellingen",
"HeaderPlaylist": "Afspeellijst",
"HeaderPlaylistItems": "Onderdelen in afspeellijst",
"HeaderProgressSyncFailed": "Voortgang synchronisatie mislukt",
"HeaderRSSFeed": "RSS Feed",
"HeaderRSSFeedGeneral": "RSS-details",
"HeaderRSSFeedIsOpen": "RSS-feed is open",
@@ -162,6 +166,7 @@
"LabelInternalAppStorage": "Interne App Opslag",
"LabelJumpBackwardsTime": "Terug in tijd springen",
"LabelJumpForwardsTime": "Verder in tijd springen",
"LabelKeepScreenAwake": "Scherm actief houden",
"LabelLanguage": "Taal",
"LabelLayout": "Layout",
"LabelLayoutAuto": "Automatisch",
@@ -176,6 +181,7 @@
"LabelLow": "Laag",
"LabelMediaType": "Mediatype",
"LabelMedium": "Medium",
"LabelMissing": "Missende",
"LabelMore": "Meer",
"LabelMoreInfo": "Meer info",
"LabelName": "Naam",
@@ -193,6 +199,8 @@
"LabelNotFinished": "Niet Voltooid",
"LabelNotStarted": "Niet Gestart",
"LabelNumEpisodes": "{0} afleveringen",
"LabelNumEpisodesIncomplete": "{0} afleveringen, {1} incompleet",
"LabelNumberOfEpisodes": "# Afleveringen",
"LabelOff": "Uit",
"LabelOn": "Aan",
"LabelPassword": "Wachtwoord",
@@ -246,6 +254,7 @@
"LabelTag": "Tag",
"LabelTags": "Tags",
"LabelTheme": "Thema",
"LabelThemeBlack": "Zwart",
"LabelThemeDark": "Donker",
"LabelThemeLight": "Licht",
"LabelTimeRemaining": "{0} te gaan",
@@ -272,6 +281,7 @@
"MessageBookshelfEmpty": "Boekenplank leeg",
"MessageConfirmDeleteLocalEpisode": "Lokale aflevering \"{0}\" van uw apparaat verwijderen? Het bestand op de server blijft onaangetast.",
"MessageConfirmDeleteLocalFiles": "Lokale bestanden van dit item van uw apparaat verwijderen? De bestanden op de server en uw voortgang worden niet beïnvloed.",
"MessageConfirmDisableAutoTimer": "Weet u zeker dat je de automatische timer voor de rest van vandaag wilt uitschakelen? De timer wordt automatisch weer ingeschakeld aan het einde van deze sluimerperiode of als u de app opnieuw start.",
"MessageConfirmDiscardProgress": "Weet u zeker dat u uw voortgang wilt resetten?",
"MessageConfirmDownloadUsingCellular": "U staat op het punt om te downloaden met behulp van mobiele data. Dit kan kosten voor data van de provider met zich meebrengen. Wilt u doorgaan?",
"MessageConfirmMarkAsFinished": "Weet u zeker dat u dit item als voltooid wilt markeren?",
@@ -300,12 +310,14 @@
"MessageNoItems": "Geen onderdelen",
"MessageNoItemsFound": "Geen onderdelen gevonden",
"MessageNoListeningSessions": "Geen luistersessies",
"MessageNoLogs": "Geen log bestanden",
"MessageNoMediaFolders": "Geen Media Folders",
"MessageNoNetworkConnection": "Geen netwerk connectie",
"MessageNoPodcastsFound": "Geen podcasts gevonden",
"MessageNoSeries": "Geen series",
"MessageNoUpdatesWereNecessary": "Geen bijwerkingen waren noodzakelijk",
"MessageNoUserPlaylists": "Je hebt geen afspeellijsten",
"MessageOldServerConnectionWarning": "Server connectie configuratie gebruikt een oud user ID. Gelieve deze server connectie te verwijderen en opnieuw toe te voegen.",
"MessagePodcastSearchField": "Zoekterm of RSS feed URL invullen",
"MessageReportBugsAndContribute": "Rapporteer bugs, vraag functionaliteiten aan en draag bij op",
"MessageSeriesAlreadyDownloaded": "Je hebt alle boeken in deze serie al gedownload.",
+5
View File
@@ -6,6 +6,7 @@
"ButtonCancel": "Отмена",
"ButtonCancelTimer": "Отмена таймера",
"ButtonClearFilter": "Очистить фильтр",
"ButtonClearLogs": "Очистить журнал",
"ButtonCloseFeed": "Закрыть канал",
"ButtonCollections": "Коллекции",
"ButtonConnect": "Подключение",
@@ -26,7 +27,9 @@
"ButtonLatest": "Последнее",
"ButtonLibrary": "Библиотека",
"ButtonLocalMedia": "Локальные медиа",
"ButtonLogs": "Журнал",
"ButtonManageLocalFiles": "Управление локальными файлами",
"ButtonMaskServerAddress": "Скрывать адрес сервера",
"ButtonNewFolder": "Новая папка",
"ButtonNextEpisode": "Следующий эпизод",
"ButtonOk": "Ок",
@@ -50,6 +53,7 @@
"ButtonStream": "Стрим",
"ButtonSubmit": "Применить",
"ButtonSwitchServerUser": "Изменить Сервер/Пользователя",
"ButtonUnmaskServerAddress": "Не скрывать адрес сервера",
"ButtonUserStats": "Статистика пользователя",
"ButtonYes": "Да",
"HeaderAccount": "Учетная запись",
@@ -310,6 +314,7 @@
"MessageNoItems": "Нет элементов",
"MessageNoItemsFound": "Элементы не найдены",
"MessageNoListeningSessions": "Нет сеансов прослушивания",
"MessageNoLogs": "Нет записей",
"MessageNoMediaFolders": "Нет папок мультимедиа",
"MessageNoNetworkConnection": "Нет подключения к сети",
"MessageNoPodcastsFound": "Подкасты не найдены",
+5
View File
@@ -6,6 +6,7 @@
"ButtonCancel": "Prekliči",
"ButtonCancelTimer": "Prekliči časovnik",
"ButtonClearFilter": "Počisti filter",
"ButtonClearLogs": "Počisti dnevnike",
"ButtonCloseFeed": "Zapri vir",
"ButtonCollections": "Zbirke",
"ButtonConnect": "Poveži",
@@ -26,7 +27,9 @@
"ButtonLatest": "Najnovejše",
"ButtonLibrary": "Knjižnica",
"ButtonLocalMedia": "Lokalni mediji",
"ButtonLogs": "Dnevniki",
"ButtonManageLocalFiles": "Upravljanje lokalnih datotek",
"ButtonMaskServerAddress": "Zakrij naslov strežnika",
"ButtonNewFolder": "Nova mapa",
"ButtonNextEpisode": "Naslednja epizoda",
"ButtonOk": "V redu",
@@ -50,6 +53,7 @@
"ButtonStream": "Pretakaj",
"ButtonSubmit": "Potrdi",
"ButtonSwitchServerUser": "Preklopi strežnik/uporabnik",
"ButtonUnmaskServerAddress": "Odkrij naslov strežnika",
"ButtonUserStats": "Uporabniška statistika",
"ButtonYes": "Da",
"HeaderAccount": "Račun",
@@ -310,6 +314,7 @@
"MessageNoItems": "Ni elementov",
"MessageNoItemsFound": "Ni najdenih elementov",
"MessageNoListeningSessions": "Ni sej poslušanja",
"MessageNoLogs": "Ni dnevnikov",
"MessageNoMediaFolders": "Brez medijskih map",
"MessageNoNetworkConnection": "Ni omrežne povezave",
"MessageNoPodcastsFound": "Ni podcastov",
+5
View File
@@ -6,6 +6,7 @@
"ButtonCancel": "Скасувати",
"ButtonCancelTimer": "Скасувати таймер",
"ButtonClearFilter": "Скинути фільтри",
"ButtonClearLogs": "Очистити журнали",
"ButtonCloseFeed": "Закрити стрічку",
"ButtonCollections": "Добірки",
"ButtonConnect": "Підключитися",
@@ -26,7 +27,9 @@
"ButtonLatest": "Останні",
"ButtonLibrary": "Бібліотека",
"ButtonLocalMedia": "Локальні файли",
"ButtonLogs": "Журнали",
"ButtonManageLocalFiles": "Керування локальними файлами",
"ButtonMaskServerAddress": "Маска адреси сервера",
"ButtonNewFolder": "Нова тека",
"ButtonNextEpisode": "Наступний епізод",
"ButtonOk": "Добре",
@@ -50,6 +53,7 @@
"ButtonStream": "Транслювати",
"ButtonSubmit": "Ввести",
"ButtonSwitchServerUser": "Змінити сервер/користувача",
"ButtonUnmaskServerAddress": "Розкрити адресу сервера",
"ButtonUserStats": "Статистика",
"ButtonYes": "Так",
"HeaderAccount": "Профіль",
@@ -310,6 +314,7 @@
"MessageNoItems": "Елементи відсутні",
"MessageNoItemsFound": "Елементів не знайдено",
"MessageNoListeningSessions": "Сеанси прослуховування відсутні",
"MessageNoLogs": "Без журналів",
"MessageNoMediaFolders": "Теки медіа відсутні",
"MessageNoNetworkConnection": "Немає підключення до мережі",
"MessageNoPodcastsFound": "Подкастів не знайдено",
+5
View File
@@ -6,6 +6,7 @@
"ButtonCancel": "取消",
"ButtonCancelTimer": "取消计时器",
"ButtonClearFilter": "清除过滤器",
"ButtonClearLogs": "清除日志",
"ButtonCloseFeed": "关闭源",
"ButtonCollections": "收藏",
"ButtonConnect": "连接",
@@ -26,7 +27,9 @@
"ButtonLatest": "最新",
"ButtonLibrary": "媒体库",
"ButtonLocalMedia": "本地媒体",
"ButtonLogs": "日志",
"ButtonManageLocalFiles": "管理本地文件",
"ButtonMaskServerAddress": "屏蔽服务器地址",
"ButtonNewFolder": "新建文件夹",
"ButtonNextEpisode": "下一集",
"ButtonOk": "确定",
@@ -50,6 +53,7 @@
"ButtonStream": "串流播放",
"ButtonSubmit": "提交",
"ButtonSwitchServerUser": "切换服务器/用户",
"ButtonUnmaskServerAddress": "取消屏蔽服务器地址",
"ButtonUserStats": "用户统计信息",
"ButtonYes": "确定",
"HeaderAccount": "帐户",
@@ -310,6 +314,7 @@
"MessageNoItems": "无项目",
"MessageNoItemsFound": "未找到任何项目",
"MessageNoListeningSessions": "无收听会话",
"MessageNoLogs": "无日志",
"MessageNoMediaFolders": "没有媒体文件夹",
"MessageNoNetworkConnection": "无网络连接",
"MessageNoPodcastsFound": "未找到播客",