Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6bbcd300e4 | ||
|
|
c54d9b3c5f | ||
|
|
9ab03fbcd4 | ||
|
|
a852114e9c | ||
|
|
3c9b8c1c6f | ||
|
|
4735b51dc0 | ||
|
|
23927a1d59 | ||
|
|
40b592f7c1 | ||
|
|
4da31f5850 | ||
|
|
f79e96cbfe | ||
|
|
2dbce1e3bb | ||
|
|
cdfbbcbf8a | ||
|
|
890852bef5 | ||
|
|
3b285db21a | ||
|
|
d37be88977 | ||
|
|
e6d27a1487 | ||
|
|
5d23c17d30 | ||
|
|
9c9c1c3b94 | ||
|
|
4e94fd6ad0 | ||
|
|
81a8203cca | ||
|
|
b6b206c288 | ||
|
|
ed2310a2e7 | ||
|
|
bae5d97087 | ||
|
|
3e12ff139b | ||
|
|
7ccd8db085 | ||
|
|
fdc829d06c | ||
|
|
e7f61e34e8 | ||
|
|
141877f111 | ||
|
|
b71a976e61 | ||
|
|
68c95ed6d5 | ||
|
|
7a68cc99af | ||
|
|
7294bc7033 | ||
|
|
f8836be147 | ||
|
|
713999eb83 | ||
|
|
cc4c9787c0 | ||
|
|
ecc5cf3e1b | ||
|
|
ba7d2be03e | ||
|
|
368f349c78 | ||
|
|
639d641c07 | ||
|
|
d9313a1654 | ||
|
|
9dd532285c | ||
|
|
92f10fdb5c | ||
|
|
9d0f43a664 | ||
|
|
453365fcd6 | ||
|
|
6231c5fcc4 |
@@ -33,8 +33,8 @@ android {
|
||||
applicationId "com.audiobookshelf.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 88
|
||||
versionName "0.9.57-beta"
|
||||
versionCode 89
|
||||
versionName "0.9.59-beta"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
@@ -43,6 +43,12 @@ android {
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
versionNameSuffix "-debug"
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 879 B |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 685 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 16 KiB |
@@ -40,7 +40,7 @@ data class DeviceSettings(
|
||||
@get:JsonIgnore
|
||||
val jumpBackwardsTimeMs get() = (jumpBackwardsTime ?: default().jumpBackwardsTime) * 1000L
|
||||
@get:JsonIgnore
|
||||
val jumpForwardTimeMs get() = (jumpForwardTime ?: default().jumpBackwardsTime) * 1000L
|
||||
val jumpForwardTimeMs get() = (jumpForwardTime ?: default().jumpForwardTime) * 1000L
|
||||
}
|
||||
|
||||
data class DeviceData(
|
||||
|
||||
@@ -239,6 +239,8 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
||||
when (action) {
|
||||
CUSTOM_ACTION_JUMP_FORWARD -> onFastForward()
|
||||
CUSTOM_ACTION_JUMP_BACKWARD -> onRewind()
|
||||
CUSTOM_ACTION_SKIP_FORWARD -> onSkipToNext()
|
||||
CUSTOM_ACTION_SKIP_BACKWARD -> onSkipToPrevious()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,3 +2,5 @@ package com.audiobookshelf.app.player
|
||||
|
||||
const val CUSTOM_ACTION_JUMP_FORWARD = "com.audiobookshelf.customAction.jump_forward";
|
||||
const val CUSTOM_ACTION_JUMP_BACKWARD = "com.audiobookshelf.customAction.jump_backward";
|
||||
const val CUSTOM_ACTION_SKIP_FORWARD = "com.audiobookshelf.customAction.skip_forward";
|
||||
const val CUSTOM_ACTION_SKIP_BACKWARD = "com.audiobookshelf.customAction.skip_backward";
|
||||
|
||||
@@ -45,6 +45,8 @@ import kotlin.concurrent.schedule
|
||||
|
||||
|
||||
const val SLEEP_TIMER_WAKE_UP_EXPIRATION = 120000L // 2m
|
||||
const val PLAYER_CAST = "cast-player";
|
||||
const val PLAYER_EXO = "exo-player";
|
||||
|
||||
class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
|
||||
@@ -256,6 +258,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
|
||||
mediaSessionConnector = MediaSessionConnector(mediaSession)
|
||||
val queueNavigator: TimelineQueueNavigator = object : TimelineQueueNavigator(mediaSession) {
|
||||
override fun getSupportedQueueNavigatorActions(player: Player): Long {
|
||||
return PlaybackStateCompat.ACTION_PLAY_PAUSE or PlaybackStateCompat.ACTION_PLAY or PlaybackStateCompat.ACTION_PAUSE
|
||||
}
|
||||
|
||||
override fun getMediaDescription(player: Player, windowIndex: Int): MediaDescriptionCompat {
|
||||
if (currentPlaybackSession == null) {
|
||||
Log.e(tag,"Playback session is not set - returning blank MediaDescriptionCompat")
|
||||
@@ -296,11 +302,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
mediaSessionConnector.setQueueNavigator(queueNavigator)
|
||||
mediaSessionConnector.setPlaybackPreparer(MediaSessionPlaybackPreparer(this))
|
||||
|
||||
mediaSessionConnector.setCustomActionProviders(
|
||||
JumpForwardCustomActionProvider(),
|
||||
JumpBackwardCustomActionProvider(),
|
||||
)
|
||||
|
||||
mediaSession.setCallback(MediaSessionCallback(this))
|
||||
|
||||
initializeMPlayer()
|
||||
@@ -343,19 +344,34 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
}
|
||||
|
||||
isClosed = false
|
||||
val customActionProviders = mutableListOf(
|
||||
JumpBackwardCustomActionProvider(),
|
||||
JumpForwardCustomActionProvider(),
|
||||
)
|
||||
val metadata = playbackSession.getMediaMetadataCompat()
|
||||
mediaSession.setMetadata(metadata)
|
||||
val mediaItems = playbackSession.getMediaItems()
|
||||
val playbackRateToUse = playbackRate ?: initialPlaybackRate ?: 1f
|
||||
initialPlaybackRate = playbackRate
|
||||
|
||||
if (playbackSession.mediaPlayer != PLAYER_CAST && mediaItems.size > 1) {
|
||||
customActionProviders.addAll(listOf(
|
||||
SkipBackwardCustomActionProvider(),
|
||||
SkipForwardCustomActionProvider(),
|
||||
));
|
||||
}
|
||||
mediaSessionConnector.setCustomActionProviders(*customActionProviders.toTypedArray());
|
||||
|
||||
playbackSession.mediaPlayer = getMediaPlayer()
|
||||
|
||||
if (playbackSession.mediaPlayer == "cast-player" && playbackSession.isLocal) {
|
||||
if (playbackSession.mediaPlayer == PLAYER_CAST && playbackSession.isLocal) {
|
||||
Log.w(tag, "Cannot cast local media item - switching player")
|
||||
currentPlaybackSession = null
|
||||
switchToPlayer(false)
|
||||
playbackSession.mediaPlayer = getMediaPlayer()
|
||||
}
|
||||
|
||||
if (playbackSession.mediaPlayer == "cast-player") {
|
||||
if (playbackSession.mediaPlayer == PLAYER_CAST) {
|
||||
// If cast-player is the first player to be used
|
||||
mediaSessionConnector.setPlayer(castPlayer)
|
||||
playerNotificationManager.setPlayer(castPlayer)
|
||||
@@ -366,10 +382,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
|
||||
clientEventEmitter?.onPlaybackSession(playbackSession)
|
||||
|
||||
val metadata = playbackSession.getMediaMetadataCompat()
|
||||
mediaSession.setMetadata(metadata)
|
||||
val mediaItems = playbackSession.getMediaItems()
|
||||
|
||||
if (mediaItems.isEmpty()) {
|
||||
Log.e(tag, "Invalid playback session no media items to play")
|
||||
currentPlaybackSession = null
|
||||
@@ -751,7 +763,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
}
|
||||
|
||||
fun getMediaPlayer():String {
|
||||
return if(currentPlayer == castPlayer) "cast-player" else "exo-player"
|
||||
return if(currentPlayer == castPlayer) PLAYER_CAST else PLAYER_EXO
|
||||
}
|
||||
|
||||
fun getDeviceInfo(): DeviceInfo {
|
||||
@@ -1024,5 +1036,39 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
).build()
|
||||
}
|
||||
}
|
||||
|
||||
inner class SkipForwardCustomActionProvider : CustomActionProvider {
|
||||
override fun onCustomAction(player: Player, action: String, extras: Bundle?) {
|
||||
/*
|
||||
This does not appear to ever get called. Instead, MediaSessionCallback.onCustomAction() is
|
||||
responsible to reacting to a custom action.
|
||||
*/
|
||||
}
|
||||
|
||||
override fun getCustomAction(player: Player): PlaybackStateCompat.CustomAction? {
|
||||
return PlaybackStateCompat.CustomAction.Builder(
|
||||
CUSTOM_ACTION_SKIP_FORWARD,
|
||||
getContext().getString(R.string.action_skip_forward),
|
||||
R.drawable.skip_next_24
|
||||
).build()
|
||||
}
|
||||
}
|
||||
|
||||
inner class SkipBackwardCustomActionProvider : CustomActionProvider {
|
||||
override fun onCustomAction(player: Player, action: String, extras: Bundle?) {
|
||||
/*
|
||||
This does not appear to ever get called. Instead, MediaSessionCallback.onCustomAction() is
|
||||
responsible to reacting to a custom action.
|
||||
*/
|
||||
}
|
||||
|
||||
override fun getCustomAction(player: Player): PlaybackStateCompat.CustomAction? {
|
||||
return PlaybackStateCompat.CustomAction.Builder(
|
||||
CUSTOM_ACTION_SKIP_BACKWARD,
|
||||
getContext().getString(R.string.action_skip_backward),
|
||||
R.drawable.skip_previous_24
|
||||
).build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="#FFFFFF">
|
||||
<group android:scaleX="1.127451"
|
||||
android:scaleY="1.127451"
|
||||
android:translateX="-1.5294118"
|
||||
android:translateY="-1.5294118">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M6,18l8.5,-6L6,6v12zM16,6v12h2V6h-2z"/>
|
||||
</group>
|
||||
</vector>
|
||||
@@ -0,0 +1,15 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="#FFFFFF">
|
||||
<group android:scaleX="1.127451"
|
||||
android:scaleY="1.127451"
|
||||
android:translateX="-1.5294118"
|
||||
android:translateY="-1.5294118">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M6,6h2v12L6,18zM9.5,12l8.5,6L18,6z"/>
|
||||
</group>
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="#FFFFFF">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M6,18l8.5,-6L6,6v12zM16,6v12h2V6h-2z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="#FFFFFF">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M6,6h2v12L6,18zM9.5,12l8.5,6L18,6z"/>
|
||||
</vector>
|
||||
@@ -11,6 +11,8 @@
|
||||
<item name="windowActionBar">false</item>
|
||||
<item name="windowNoTitle">true</item>
|
||||
<item name="android:background">@null</item>
|
||||
<item name="android:statusBarColor">@color/background_dark</item>
|
||||
<item name="android:navigationBarColor">@color/background_dark</item>
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -3,4 +3,5 @@
|
||||
<color name="light_blue_200">#FF81D4FA</color>
|
||||
<color name="light_blue_600">#FF039BE5</color>
|
||||
<color name="light_blue_900">#FF01579B</color>
|
||||
<color name="background_dark">#262626</color>
|
||||
</resources>
|
||||
@@ -8,4 +8,6 @@
|
||||
<string name="app_widget_description">Simple widget for audiobookshelf playback</string>
|
||||
<string name="action_jump_forward">Jump Forward</string>
|
||||
<string name="action_jump_backward">Jump Backward</string>
|
||||
<string name="action_skip_forward">Skip Forward</string>
|
||||
<string name="action_skip_backward">Skip Backward</string>
|
||||
</resources>
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
<item name="windowActionBar">false</item>
|
||||
<item name="windowNoTitle">true</item>
|
||||
<item name="android:background">@null</item>
|
||||
<item name="android:statusBarColor">@color/background_dark</item>
|
||||
<item name="android:navigationBarColor">@color/background_dark</item>
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
@font-face {
|
||||
font-family: 'absicons';
|
||||
src: url('~static/fonts/absicons/absicons.eot?2jfq33');
|
||||
src: url('~static/fonts/absicons/absicons.eot?2jfq33#iefix') format('embedded-opentype'),
|
||||
url('~static/fonts/absicons/absicons.ttf?2jfq33') format('truetype'),
|
||||
url('~static/fonts/absicons/absicons.woff?2jfq33') format('woff'),
|
||||
url('~static/fonts/absicons/absicons.svg?2jfq33#absicons') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
}
|
||||
|
||||
.abs-icons {
|
||||
/* use !important to prevent issues with browser extensions that change fonts */
|
||||
font-family: 'absicons' !important;
|
||||
speak: never;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
|
||||
/* Better Font Rendering =========== */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-books-1:before {
|
||||
content: "\e905";
|
||||
}
|
||||
|
||||
.icon-microphone-1:before {
|
||||
content: "\e902";
|
||||
}
|
||||
|
||||
.icon-radio:before {
|
||||
content: "\e903";
|
||||
}
|
||||
|
||||
.icon-podcast:before {
|
||||
content: "\e904";
|
||||
}
|
||||
|
||||
.icon-audiobookshelf:before {
|
||||
content: "\e900";
|
||||
}
|
||||
|
||||
.icon-database:before {
|
||||
content: "\e906";
|
||||
}
|
||||
|
||||
.icon-microphone-2:before {
|
||||
content: "\e901";
|
||||
}
|
||||
|
||||
.icon-headphones:before {
|
||||
content: "\e910";
|
||||
}
|
||||
|
||||
.icon-music:before {
|
||||
content: "\e911";
|
||||
}
|
||||
|
||||
.icon-video:before {
|
||||
content: "\e914";
|
||||
}
|
||||
|
||||
.icon-microphone-3:before {
|
||||
content: "\e91e";
|
||||
}
|
||||
|
||||
.icon-book-1:before {
|
||||
content: "\e91f";
|
||||
}
|
||||
|
||||
.icon-books-2:before {
|
||||
content: "\e920";
|
||||
}
|
||||
|
||||
.icon-file-picture:before {
|
||||
content: "\e927";
|
||||
}
|
||||
|
||||
.icon-database-1:before {
|
||||
content: "\e964";
|
||||
}
|
||||
|
||||
.icon-rocket:before {
|
||||
content: "\e9a5";
|
||||
}
|
||||
|
||||
.icon-power:before {
|
||||
content: "\e9b5";
|
||||
}
|
||||
|
||||
.icon-star:before {
|
||||
content: "\e9d9";
|
||||
}
|
||||
|
||||
.icon-heart:before {
|
||||
content: "\e9da";
|
||||
}
|
||||
|
||||
.icon-rss:before {
|
||||
content: "\ea9b";
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
@import "./fonts.css";
|
||||
@import './defaultStyles.css';
|
||||
@import './absicons.css';
|
||||
|
||||
body {
|
||||
background-color: #262626;
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
</a>
|
||||
<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">
|
||||
<widgets-library-icon :icon="currentLibraryIcon" :size="4" />
|
||||
<ui-library-icon :icon="currentLibraryIcon" :size="4" font-size="base" />
|
||||
<p class="text-sm font-book leading-4 ml-2 mt-0.5 max-w-24 truncate">{{ currentLibraryName }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -205,7 +205,7 @@ export default {
|
||||
return this.$store.getters['getJumpBackwardsTime']
|
||||
},
|
||||
bookCoverAspectRatio() {
|
||||
return this.$store.getters['getBookCoverAspectRatio']
|
||||
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||
},
|
||||
bookCoverWidth() {
|
||||
if (this.showFullscreen) return this.fullscreenBookCoverWidth
|
||||
|
||||
@@ -155,8 +155,10 @@ export default {
|
||||
}
|
||||
|
||||
// Settings have been loaded (at least once, so it's safe to kickoff onReady)
|
||||
this.settingsLoaded = true
|
||||
this.notifyOnReady()
|
||||
if (!this.settingsLoaded) {
|
||||
this.settingsLoaded = true
|
||||
this.notifyOnReady()
|
||||
}
|
||||
},
|
||||
setListeners() {
|
||||
// if (!this.$server.socket) {
|
||||
|
||||
@@ -89,7 +89,7 @@ export default {
|
||||
return this.bookCoverAspectRatio === 1
|
||||
},
|
||||
bookCoverAspectRatio() {
|
||||
return this.$store.getters['getBookCoverAspectRatio']
|
||||
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||
},
|
||||
bookWidth() {
|
||||
var coverSize = 100
|
||||
|
||||
@@ -69,7 +69,7 @@ export default {
|
||||
return this.bookCoverAspectRatio === 1
|
||||
},
|
||||
bookCoverAspectRatio() {
|
||||
return this.$store.getters['getBookCoverAspectRatio']
|
||||
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||
},
|
||||
altViewEnabled() {
|
||||
return this.$store.getters['getAltViewEnabled']
|
||||
|
||||
@@ -31,7 +31,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
bookCoverAspectRatio() {
|
||||
return this.$store.getters['getBookCoverAspectRatio']
|
||||
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||
},
|
||||
coverWidth() {
|
||||
if (this.bookCoverAspectRatio === 1) return 50 * 1.2
|
||||
|
||||
@@ -24,7 +24,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
bookCoverAspectRatio() {
|
||||
return this.$store.getters['getBookCoverAspectRatio']
|
||||
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||
},
|
||||
name() {
|
||||
return this.series.name
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
<template>
|
||||
<svg fill="currentColor" class="h-full w-full p-px" viewBox="0 0 1978.03 2349.44">
|
||||
<path
|
||||
d="M2519.5,1438.39c-12.13-10.1-31-25-56.57-42.62V1197.31c0-505.94-410.15-916.09-916.1-916.09h0c-505.94,0-916.09,410.15-916.09,916.09v198.46c-25.57,17.66-44.44,32.52-56.57,42.62a45.45,45.45,0,0,0-16.35,34.95v237.74a45.45,45.45,0,0,0,16.35,35c28.28,23.54,93.18,72.92,194.22,123.55v23.11c0,62.32,40.21,112.85,89.8,112.85h0c49.59,0,89.8-50.53,89.8-112.85V1322.51c0-62.33-40.21-112.86-89.8-112.86h0c-47.51,0-86.4,46.38-89.58,105.07l-.22.11V1197.31c0-429.92,348.52-778.43,778.44-778.43h0c429.92,0,778.44,348.51,778.44,778.43v117.52l-.22-.11c-3.18-58.69-42.06-105.07-89.58-105.07h0c-49.59,0-89.79,50.53-89.79,112.86v570.18c0,62.32,40.2,112.85,89.79,112.85h0c49.6,0,89.8-50.53,89.8-112.85v-23.11c101.05-50.63,165.95-100,194.23-123.55a45.48,45.48,0,0,0,16.35-35V1473.34A45.48,45.48,0,0,0,2519.5,1438.39Z"
|
||||
transform="translate(-557.82 -281.22)"
|
||||
/>
|
||||
<path d="M1227.4,2429.63a108.47,108.47,0,0,0,108.47-108.47V1106.56A108.47,108.47,0,0,0,1227.4,998.08H1115.33a108.48,108.48,0,0,0-108.48,108.48v1214.6a108.47,108.47,0,0,0,108.48,108.47ZM1047.75,1289.38H1295v25.83H1047.75Z" transform="translate(-557.82 -281.22)" />
|
||||
<path d="M1602.87,2429.63a108.47,108.47,0,0,0,108.47-108.47V1106.56a108.47,108.47,0,0,0-108.47-108.48H1490.8a108.48,108.48,0,0,0-108.48,108.48v1214.6a108.47,108.47,0,0,0,108.48,108.47ZM1423.22,1289.38h247.22v25.83H1423.22Z" transform="translate(-557.82 -281.22)" />
|
||||
<path d="M1978.34,2429.63a108.47,108.47,0,0,0,108.47-108.47V1106.56a108.47,108.47,0,0,0-108.47-108.48H1866.27a108.48,108.48,0,0,0-108.48,108.48v1214.6a108.47,108.47,0,0,0,108.48,108.47ZM1798.69,1289.38h247.22v25.83H1798.69Z" transform="translate(-557.82 -281.22)" />
|
||||
<rect x="180.05" y="2185.95" width="1617.93" height="163.49" rx="81.74" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {},
|
||||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
@@ -1,16 +0,0 @@
|
||||
<template>
|
||||
<svg fill="currentColor" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M9 3V18H12V3H9M12 5L16 18L19 17L15 4L12 5M5 5V18H8V5H5M3 19V21H21V19H3Z" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {},
|
||||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
@@ -1,16 +0,0 @@
|
||||
<template>
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M6,19L9,15.14L11.14,17.72L14.14,13.86L18,19H6M6,4H11V12L8.5,10.5L6,12M18,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V4A2,2 0 0,0 18,2Z" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {},
|
||||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
@@ -1,16 +0,0 @@
|
||||
<template>
|
||||
<svg class="p-px" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {},
|
||||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
@@ -1,19 +0,0 @@
|
||||
<template>
|
||||
<svg class="p-px" viewBox="0 0 122.877 120.596">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M68.925,69.906v50.689H53.953V69.906c-4.918-2.662-8.259-7.867-8.259-13.854 c0-8.694,7.05-15.744,15.745-15.744c8.694,0,15.745,7.05,15.745,15.744C77.184,62.039,73.843,67.244,68.925,69.906L68.925,69.906z M39.32,11.165c2.916-1.438,4.111-4.969,2.673-7.882c-1.438-2.914-4.966-4.111-7.88-2.674C22.213,6.479,12.958,16.19,7.11,27.625 c-4.32,8.445-6.783,17.842-7.08,27.325c-0.299,9.563,1.587,19.223,5.973,28.114c5.401,10.953,14.558,20.695,28.039,27.592 c2.889,1.477,6.429,0.33,7.905-2.559c1.477-2.889,0.331-6.428-2.558-7.904c-11.037-5.645-18.486-13.525-22.833-22.334 c-3.506-7.111-5.014-14.857-4.774-22.539c0.243-7.757,2.256-15.442,5.79-22.348C22.304,23.721,29.76,15.879,39.32,11.165 L39.32,11.165z M88.765,0.608c-2.914-1.438-6.443-0.24-7.881,2.674c-1.438,2.914-0.242,6.445,2.674,7.882 c9.561,4.715,17.017,12.556,21.747,21.808c3.533,6.905,5.547,14.59,5.789,22.348c0.24,7.682-1.268,15.428-4.773,22.539 c-4.347,8.809-11.796,16.689-22.833,22.334c-2.889,1.477-4.034,5.016-2.558,7.904c1.476,2.889,5.016,4.035,7.905,2.559 c13.48-6.896,22.638-16.639,28.039-27.592c4.386-8.891,6.272-18.551,5.973-28.114c-0.297-9.483-2.76-18.88-7.079-27.325 C109.919,16.19,100.665,6.479,88.765,0.608L88.765,0.608z M82.791,26.505c-2.195-1.581-5.256-1.082-6.837,1.113 c-1.58,2.195-1.082,5.256,1.113,6.837c0.885,0.637,1.753,1.352,2.604,2.134c4.971,4.583,7.919,10.694,8.538,17.16 c0.626,6.524-1.111,13.437-5.518,19.552c-0.748,1.039-1.61,2.092-2.585,3.15c-1.835,1.992-1.708,5.098,0.287,6.932 c1.994,1.834,5.099,1.705,6.933-0.287c1.18-1.279,2.286-2.641,3.315-4.072c5.862-8.139,8.166-17.4,7.322-26.197 c-0.848-8.853-4.871-17.208-11.648-23.457C85.249,28.387,84.074,27.431,82.791,26.505L82.791,26.505z M45.81,34.458 c2.195-1.581,2.694-4.642,1.113-6.837c-1.581-2.195-4.642-2.694-6.837-1.114c-1.284,0.926-2.458,1.882-3.524,2.864 c-6.778,6.25-10.801,14.604-11.649,23.457c-0.844,8.796,1.46,18.06,7.323,26.199c1.031,1.43,2.136,2.791,3.315,4.07 c1.834,1.992,4.939,2.121,6.932,0.287c1.996-1.834,2.123-4.939,0.288-6.932c-0.975-1.059-1.837-2.111-2.585-3.15 c-4.406-6.115-6.144-13.027-5.518-19.551c0.619-6.465,3.567-12.577,8.538-17.16C44.058,35.81,44.926,35.095,45.81,34.458 L45.81,34.458z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {},
|
||||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
@@ -40,7 +40,7 @@ export default {
|
||||
}
|
||||
},
|
||||
bookCoverAspectRatio() {
|
||||
return this.$store.getters['getBookCoverAspectRatio']
|
||||
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<li :key="library.id" class="text-gray-50 select-none relative py-3 cursor-pointer hover:bg-black-400" :class="currentLibraryId === library.id ? 'bg-bg bg-opacity-80' : ''" role="option" @click="clickedOption(library)">
|
||||
<div v-show="currentLibraryId === library.id" class="absolute top-0 left-0 w-0.5 bg-warning h-full" />
|
||||
<div class="flex items-center px-3">
|
||||
<widgets-library-icon :icon="library.icon" />
|
||||
<ui-library-icon :icon="library.icon" />
|
||||
<span class="font-normal block truncate text-lg ml-4">{{ library.name }}</span>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
<script>
|
||||
import Path from 'path'
|
||||
import { Archive } from 'libarchive.js/main.js'
|
||||
import { CompressedFile } from 'libarchive.js/src/compressed-file'
|
||||
|
||||
Archive.init({
|
||||
workerUrl: '/libarchive/worker-bundle.js'
|
||||
@@ -136,7 +137,11 @@ export default {
|
||||
responseType: 'blob'
|
||||
})
|
||||
const archive = await Archive.open(buff)
|
||||
this.filesObject = await archive.getFilesObject()
|
||||
const originalFilesObject = await archive.getFilesObject()
|
||||
// to support images in subfolders we need to flatten the object
|
||||
// ref: https://github.com/advplyr/audiobookshelf/issues/811
|
||||
this.filesObject = this.flattenFilesObject(originalFilesObject)
|
||||
console.log('Extracted files object', this.filesObject)
|
||||
var filenames = Object.keys(this.filesObject)
|
||||
this.parseFilenames(filenames)
|
||||
|
||||
@@ -154,6 +159,26 @@ export default {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
flattenFilesObject(filesObject) {
|
||||
const flattenObject = (obj, prefix = '') => {
|
||||
var _obj = {}
|
||||
for (const key in obj) {
|
||||
const newKey = prefix ? prefix + '/' + key : key
|
||||
if (obj[key] instanceof CompressedFile) {
|
||||
_obj[newKey] = obj[key]
|
||||
} else if (!key.startsWith('_') && typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
|
||||
_obj = {
|
||||
..._obj,
|
||||
...flattenObject(obj[key], newKey)
|
||||
}
|
||||
} else {
|
||||
_obj[newKey] = obj[key]
|
||||
}
|
||||
}
|
||||
return _obj
|
||||
}
|
||||
return flattenObject(filesObject)
|
||||
},
|
||||
async extractXmlFile(filename) {
|
||||
console.log('extracting xml filename', filename)
|
||||
try {
|
||||
@@ -173,7 +198,7 @@ export default {
|
||||
},
|
||||
parseImageFilename(filename) {
|
||||
var basename = Path.basename(filename, Path.extname(filename))
|
||||
var numbersinpath = basename.match(/\d{1,4}/g)
|
||||
var numbersinpath = basename.match(/\d{1,5}/g)
|
||||
if (!numbersinpath || !numbersinpath.length) {
|
||||
return {
|
||||
index: -1,
|
||||
|
||||
@@ -50,7 +50,7 @@ export default {
|
||||
return this.$elapsedPretty(this.media.duration)
|
||||
},
|
||||
bookCoverAspectRatio() {
|
||||
return this.$store.getters['getBookCoverAspectRatio']
|
||||
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||
},
|
||||
bookWidth() {
|
||||
if (this.bookCoverAspectRatio === 1) return 50
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div :class="`h-${size} w-${size} min-w-${size} text-${fontSize}`" class="flex items-center justify-center">
|
||||
<span class="abs-icons" :class="`icon-${iconToUse}`"></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
icon: {
|
||||
type: String,
|
||||
default: 'audiobookshelf'
|
||||
},
|
||||
fontSize: {
|
||||
type: String,
|
||||
default: 'lg'
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 5
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
iconToUse() {
|
||||
return this.icons.includes(this.icon) ? this.icon : 'audiobookshelf'
|
||||
},
|
||||
icons() {
|
||||
return this.$store.state.globals.libraryIcons
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
@@ -1,28 +0,0 @@
|
||||
<template>
|
||||
<div :class="`h-${size} w-${size}`">
|
||||
<component :is="iconComponentName" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
icon: String,
|
||||
size: {
|
||||
type: Number,
|
||||
default: 5
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
iconComponentName() {
|
||||
if (this.icon === 'default') return `icons-database-svg`
|
||||
return `icons-${this.icon}-svg`
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
@@ -34,7 +34,7 @@
|
||||
504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; };
|
||||
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
|
||||
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
|
||||
A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */; };
|
||||
AC98F95F300E46A27FAE47A4 /* Pods_Audiobookshelf.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B775656E49492CFD6763B7B6 /* Pods_Audiobookshelf.framework */; };
|
||||
E9D5504628AC1A3900C746DD /* LibraryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5504528AC1A3900C746DD /* LibraryItem.swift */; };
|
||||
E9D5504828AC1A7A00C746DD /* MediaType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5504728AC1A7A00C746DD /* MediaType.swift */; };
|
||||
E9D5504A28AC1AA600C746DD /* Metadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5504928AC1AA600C746DD /* Metadata.swift */; };
|
||||
@@ -59,9 +59,22 @@
|
||||
E9D5507328AC218300C746DD /* DaoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5507228AC218300C746DD /* DaoExtensions.swift */; };
|
||||
E9D5507528AEF93100C746DD /* PlayerSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5507428AEF93100C746DD /* PlayerSettings.swift */; };
|
||||
E9DFCBFB28C28F4A00B36356 /* AudioPlayerSleepTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9DFCBFA28C28F4A00B36356 /* AudioPlayerSleepTimer.swift */; };
|
||||
E9E8814A28DA644F00D750C1 /* PlayerTimeUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E8814928DA644F00D750C1 /* PlayerTimeUtils.swift */; };
|
||||
E9E8814D28DA6B9000D750C1 /* PlayerTimeUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E8814C28DA6B9000D750C1 /* PlayerTimeUtilsTests.swift */; };
|
||||
E9E985F828B02D9400957F23 /* PlayerProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E985F728B02D9400957F23 /* PlayerProgress.swift */; };
|
||||
E9FA07E328C82848005520B0 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9FA07E228C82848005520B0 /* Logger.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
E9E8813E28DA5DE500D750C1 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 504EC2FC1FED79650016851F /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 504EC3031FED79650016851F;
|
||||
remoteInfo = Audiobookshelf;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = "<group>"; };
|
||||
3A200C1427D64D7E00CBF02E /* AudioPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayer.swift; sourceTree = "<group>"; };
|
||||
@@ -86,15 +99,17 @@
|
||||
4D8D412C26E187E400BA5F0D /* App-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "App-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
4DF74911287105C600AC7814 /* DeviceSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceSettings.swift; sourceTree = "<group>"; };
|
||||
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
|
||||
504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
504EC3041FED79650016851F /* Audiobookshelf.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Audiobookshelf.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
504EC30C1FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
504EC30E1FED79650016851F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
|
||||
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
5F7EC3E03050241EA35154B3 /* Pods-Audiobookshelf.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Audiobookshelf.release.xcconfig"; path = "Pods/Target Support Files/Pods-Audiobookshelf/Pods-Audiobookshelf.release.xcconfig"; sourceTree = "<group>"; };
|
||||
8FBD5FE4076DDFA0DA782256 /* Pods-Audiobookshelf.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Audiobookshelf.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Audiobookshelf/Pods-Audiobookshelf.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = "<group>"; };
|
||||
B775656E49492CFD6763B7B6 /* Pods_Audiobookshelf.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Audiobookshelf.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
E9D5504528AC1A3900C746DD /* LibraryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryItem.swift; sourceTree = "<group>"; };
|
||||
E9D5504728AC1A7A00C746DD /* MediaType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaType.swift; sourceTree = "<group>"; };
|
||||
E9D5504928AC1AA600C746DD /* Metadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Metadata.swift; sourceTree = "<group>"; };
|
||||
@@ -119,7 +134,11 @@
|
||||
E9D5507228AC218300C746DD /* DaoExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaoExtensions.swift; sourceTree = "<group>"; };
|
||||
E9D5507428AEF93100C746DD /* PlayerSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerSettings.swift; sourceTree = "<group>"; };
|
||||
E9DFCBFA28C28F4A00B36356 /* AudioPlayerSleepTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerSleepTimer.swift; sourceTree = "<group>"; };
|
||||
E9E8813A28DA5DE500D750C1 /* AudiobookshelfUnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AudiobookshelfUnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
E9E8814928DA644F00D750C1 /* PlayerTimeUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerTimeUtils.swift; sourceTree = "<group>"; };
|
||||
E9E8814C28DA6B9000D750C1 /* PlayerTimeUtilsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerTimeUtilsTests.swift; sourceTree = "<group>"; };
|
||||
E9E985F728B02D9400957F23 /* PlayerProgress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerProgress.swift; sourceTree = "<group>"; };
|
||||
E9FA07E228C82848005520B0 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
|
||||
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
@@ -128,7 +147,14 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */,
|
||||
AC98F95F300E46A27FAE47A4 /* Pods_Audiobookshelf.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
E9E8813728DA5DE500D750C1 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -138,7 +164,7 @@
|
||||
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */,
|
||||
B775656E49492CFD6763B7B6 /* Pods_Audiobookshelf.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
@@ -146,6 +172,7 @@
|
||||
3ABF6190280432610070250E /* player */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E9E8814828DA641B00D750C1 /* util */,
|
||||
3A200C1427D64D7E00CBF02E /* AudioPlayer.swift */,
|
||||
E9DFCBFA28C28F4A00B36356 /* AudioPlayerSleepTimer.swift */,
|
||||
3ABF618E2804325C0070250E /* PlayerHandler.swift */,
|
||||
@@ -199,6 +226,7 @@
|
||||
3AFCB5E627EA23F700ECCC05 /* util */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E9FA07E228C82848005520B0 /* Logger.swift */,
|
||||
3AFCB5E727EA240D00ECCC05 /* NowPlayingInfo.swift */,
|
||||
3AD4FCEA280443DD006DB301 /* Database.swift */,
|
||||
3AD4FCEC28044E6C006DB301 /* Store.swift */,
|
||||
@@ -215,6 +243,7 @@
|
||||
children = (
|
||||
3AC8248B27F2316900529205 /* Shared */,
|
||||
504EC3061FED79650016851F /* App */,
|
||||
E9E8813B28DA5DE500D750C1 /* AudiobookshelfUnitTests */,
|
||||
504EC3051FED79650016851F /* Products */,
|
||||
7F8756D8B27F46E3366F6CEA /* Pods */,
|
||||
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */,
|
||||
@@ -224,7 +253,8 @@
|
||||
504EC3051FED79650016851F /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
504EC3041FED79650016851F /* App.app */,
|
||||
504EC3041FED79650016851F /* Audiobookshelf.app */,
|
||||
E9E8813A28DA5DE500D750C1 /* AudiobookshelfUnitTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -251,6 +281,8 @@
|
||||
children = (
|
||||
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */,
|
||||
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */,
|
||||
8FBD5FE4076DDFA0DA782256 /* Pods-Audiobookshelf.debug.xcconfig */,
|
||||
5F7EC3E03050241EA35154B3 /* Pods-Audiobookshelf.release.xcconfig */,
|
||||
);
|
||||
name = Pods;
|
||||
sourceTree = "<group>";
|
||||
@@ -295,12 +327,52 @@
|
||||
path = download;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E9E8813B28DA5DE500D750C1 /* AudiobookshelfUnitTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E9E8814328DA5E5900D750C1 /* Shared */,
|
||||
);
|
||||
path = AudiobookshelfUnitTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E9E8814328DA5E5900D750C1 /* Shared */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E9E8814428DA5E6000D750C1 /* player */,
|
||||
);
|
||||
path = Shared;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E9E8814428DA5E6000D750C1 /* player */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E9E8814B28DA6B6B00D750C1 /* util */,
|
||||
);
|
||||
path = player;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E9E8814828DA641B00D750C1 /* util */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E9E8814928DA644F00D750C1 /* PlayerTimeUtils.swift */,
|
||||
);
|
||||
path = util;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E9E8814B28DA6B6B00D750C1 /* util */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E9E8814C28DA6B9000D750C1 /* PlayerTimeUtilsTests.swift */,
|
||||
);
|
||||
path = util;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
504EC3031FED79650016851F /* App */ = {
|
||||
504EC3031FED79650016851F /* Audiobookshelf */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */;
|
||||
buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "Audiobookshelf" */;
|
||||
buildPhases = (
|
||||
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */,
|
||||
504EC3001FED79650016851F /* Sources */,
|
||||
@@ -312,18 +384,36 @@
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = App;
|
||||
name = Audiobookshelf;
|
||||
productName = App;
|
||||
productReference = 504EC3041FED79650016851F /* App.app */;
|
||||
productReference = 504EC3041FED79650016851F /* Audiobookshelf.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
E9E8813928DA5DE500D750C1 /* AudiobookshelfUnitTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = E9E8814228DA5DE500D750C1 /* Build configuration list for PBXNativeTarget "AudiobookshelfUnitTests" */;
|
||||
buildPhases = (
|
||||
E9E8813628DA5DE500D750C1 /* Sources */,
|
||||
E9E8813728DA5DE500D750C1 /* Frameworks */,
|
||||
E9E8813828DA5DE500D750C1 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
E9E8813F28DA5DE500D750C1 /* PBXTargetDependency */,
|
||||
);
|
||||
name = AudiobookshelfUnitTests;
|
||||
productName = AudiobookshelfUnitTests;
|
||||
productReference = E9E8813A28DA5DE500D750C1 /* AudiobookshelfUnitTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
504EC2FC1FED79650016851F /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 920;
|
||||
LastSwiftUpdateCheck = 1400;
|
||||
LastUpgradeCheck = 1330;
|
||||
TargetAttributes = {
|
||||
504EC3031FED79650016851F = {
|
||||
@@ -331,6 +421,11 @@
|
||||
LastSwiftMigration = 1240;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
E9E8813928DA5DE500D750C1 = {
|
||||
CreatedOnToolsVersion = 14.0;
|
||||
ProvisioningStyle = Automatic;
|
||||
TestTargetID = 504EC3031FED79650016851F;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */;
|
||||
@@ -346,7 +441,8 @@
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
504EC3031FED79650016851F /* App */,
|
||||
504EC3031FED79650016851F /* Audiobookshelf */,
|
||||
E9E8813928DA5DE500D750C1 /* AudiobookshelfUnitTests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
@@ -365,6 +461,13 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
E9E8813828DA5DE500D750C1 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
@@ -379,7 +482,7 @@
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-App-checkManifestLockResult.txt",
|
||||
"$(DERIVED_FILE_DIR)/Pods-Audiobookshelf-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
@@ -398,7 +501,7 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App/Pods-App-frameworks.sh\"\n";
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Audiobookshelf/Pods-Audiobookshelf-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
@@ -412,6 +515,7 @@
|
||||
E9D5506228AC1CC900C746DD /* PlayerState.swift in Sources */,
|
||||
3AD4FCE728043E72006DB301 /* AbsDatabase.m in Sources */,
|
||||
504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
|
||||
E9FA07E328C82848005520B0 /* Logger.swift in Sources */,
|
||||
3A90295F280968E700E1D427 /* PlaybackReport.swift in Sources */,
|
||||
E9D5505A28AC1C4500C746DD /* Folder.swift in Sources */,
|
||||
3ABF580928059BAE005DFBE5 /* PlaybackSession.swift in Sources */,
|
||||
@@ -452,13 +556,30 @@
|
||||
3AF1970C2806E2590096F747 /* ApiClient.swift in Sources */,
|
||||
4D66B954282EE87C008272D4 /* AbsDownloader.swift in Sources */,
|
||||
E9D5505628AC1BFA00C746DD /* FileMetadata.swift in Sources */,
|
||||
E9E8814A28DA644F00D750C1 /* PlayerTimeUtils.swift in Sources */,
|
||||
3AB34055280832720039308B /* PlayerEvents.swift in Sources */,
|
||||
E9D5506C28AC1E2100C746DD /* LocalMediaProgress.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
E9E8813628DA5DE500D750C1 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E9E8814D28DA6B9000D750C1 /* PlayerTimeUtilsTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
E9E8813F28DA5DE500D750C1 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 504EC3031FED79650016851F /* Audiobookshelf */;
|
||||
targetProxy = E9E8813E28DA5DE500D750C1 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
504EC30B1FED79650016851F /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
@@ -594,17 +715,17 @@
|
||||
};
|
||||
504EC3171FED79650016851F /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */;
|
||||
baseConfigurationReference = 8FBD5FE4076DDFA0DA782256 /* Pods-Audiobookshelf.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 15;
|
||||
CURRENT_PROJECT_VERSION = 16;
|
||||
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.58;
|
||||
MARKETING_VERSION = 0.9.59;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app.dev;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -618,17 +739,17 @@
|
||||
};
|
||||
504EC3181FED79650016851F /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */;
|
||||
baseConfigurationReference = 5F7EC3E03050241EA35154B3 /* Pods-Audiobookshelf.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 15;
|
||||
CURRENT_PROJECT_VERSION = 16;
|
||||
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.58;
|
||||
MARKETING_VERSION = 0.9.59;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||
@@ -638,6 +759,49 @@
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
E9E8814028DA5DE500D750C1 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.AudiobookshelfUnitTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Audiobookshelf.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Audiobookshelf";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
E9E8814128DA5DE500D750C1 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.AudiobookshelfUnitTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Audiobookshelf.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Audiobookshelf";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
@@ -650,7 +814,7 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */ = {
|
||||
504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "Audiobookshelf" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
504EC3171FED79650016851F /* Debug */,
|
||||
@@ -659,6 +823,15 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
E9E8814228DA5DE500D750C1 /* Build configuration list for PBXNativeTarget "AudiobookshelfUnitTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
E9E8814028DA5DE500D750C1 /* Debug */,
|
||||
E9E8814128DA5DE500D750C1 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 504EC2FC1FED79650016851F /* Project object */;
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "504EC3031FED79650016851F"
|
||||
BuildableName = "App.app"
|
||||
BlueprintName = "App"
|
||||
BuildableName = "Audiobookshelf.app"
|
||||
BlueprintName = "Audiobookshelf"
|
||||
ReferencedContainer = "container:App.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
@@ -28,6 +28,17 @@
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "E9E8813928DA5DE500D750C1"
|
||||
BuildableName = "AudiobookshelfUnitTests.xctest"
|
||||
BlueprintName = "AudiobookshelfUnitTests"
|
||||
ReferencedContainer = "container:App.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
@@ -45,8 +56,8 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "504EC3031FED79650016851F"
|
||||
BuildableName = "App.app"
|
||||
BlueprintName = "App"
|
||||
BuildableName = "Audiobookshelf.app"
|
||||
BlueprintName = "Audiobookshelf"
|
||||
ReferencedContainer = "container:App.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
@@ -62,8 +73,8 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "504EC3031FED79650016851F"
|
||||
BuildableName = "App.app"
|
||||
BlueprintName = "App"
|
||||
BuildableName = "Audiobookshelf.app"
|
||||
BlueprintName = "Audiobookshelf"
|
||||
ReferencedContainer = "container:App.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
|
||||
@@ -4,8 +4,10 @@ import RealmSwift
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
private let logger = AppLogger(category: "AppDelegate")
|
||||
|
||||
var window: UIWindow?
|
||||
lazy var window: UIWindow? = UIWindow(frame: UIScreen.main.bounds)
|
||||
var backgroundCompletionHandler: (() -> Void)?
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
@@ -13,15 +15,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
let configuration = Realm.Configuration(
|
||||
schemaVersion: 4,
|
||||
migrationBlock: { migration, oldSchemaVersion in
|
||||
migrationBlock: { [weak self] migration, oldSchemaVersion in
|
||||
if (oldSchemaVersion < 1) {
|
||||
NSLog("Realm schema version was \(oldSchemaVersion)")
|
||||
self?.logger.log("Realm schema version was \(oldSchemaVersion)")
|
||||
migration.enumerateObjects(ofType: DeviceSettings.className()) { oldObject, newObject in
|
||||
newObject?["enableAltView"] = false
|
||||
}
|
||||
}
|
||||
if (oldSchemaVersion < 4) {
|
||||
NSLog("Realm schema version was \(oldSchemaVersion)... Reindexing server configs")
|
||||
self?.logger.log("Realm schema version was \(oldSchemaVersion)... Reindexing server configs")
|
||||
var indexCounter = 1
|
||||
migration.enumerateObjects(ofType: ServerConnectionConfig.className()) { oldObject, newObject in
|
||||
newObject?["index"] = indexCounter
|
||||
@@ -43,22 +45,22 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
func applicationDidEnterBackground(_ application: UIApplication) {
|
||||
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
|
||||
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
|
||||
NSLog("Audiobookself is now in the background")
|
||||
logger.log("Audiobookself is now in the background")
|
||||
}
|
||||
|
||||
func applicationWillEnterForeground(_ application: UIApplication) {
|
||||
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
|
||||
NSLog("Audiobookself is now in the foreground")
|
||||
logger.log("Audiobookself is now in the foreground")
|
||||
}
|
||||
|
||||
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
|
||||
NSLog("Audiobookself is now active")
|
||||
logger.log("Audiobookself is now active")
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ application: UIApplication) {
|
||||
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
|
||||
NSLog("Audiobookself is terminating")
|
||||
logger.log("Audiobookself is terminating")
|
||||
}
|
||||
|
||||
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
|
||||
@@ -83,10 +85,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
|
||||
let statusBarRect = UIApplication.shared.statusBarFrame
|
||||
let statusBarRect = self.window?.windowScene?.statusBarManager?.statusBarFrame
|
||||
guard let touchPoint = event?.allTouches?.first?.location(in: self.window) else { return }
|
||||
|
||||
if statusBarRect.contains(touchPoint) {
|
||||
if statusBarRect?.contains(touchPoint) ?? false {
|
||||
NotificationCenter.default.post(name: .capacitorStatusBarTapped, object: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,9 @@ import RealmSwift
|
||||
|
||||
@objc(AbsAudioPlayer)
|
||||
public class AbsAudioPlayer: CAPPlugin {
|
||||
private let logger = AppLogger(category: "AbsAudioPlayer")
|
||||
|
||||
private var initialPlayWhenReady = false
|
||||
private var isUIReady = false
|
||||
|
||||
override public func load() {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(sendMetadata), name: NSNotification.Name(PlayerEvents.update.rawValue), object: nil)
|
||||
@@ -33,20 +34,19 @@ public class AbsAudioPlayer: CAPPlugin {
|
||||
}
|
||||
|
||||
func restorePlaybackSession() async {
|
||||
// We don't need to restore if we have an active session
|
||||
guard PlayerHandler.getPlaybackSession() == nil else { return }
|
||||
|
||||
do {
|
||||
// Fetch the most recent active session
|
||||
let activeSession = try await Realm().objects(PlaybackSession.self).where({
|
||||
let activeSession = try Realm(queue: nil).objects(PlaybackSession.self).where({
|
||||
$0.isActiveSession == true && $0.serverConnectionConfigId == Store.serverConfig?.id
|
||||
}).last?.freeze()
|
||||
|
||||
if let activeSession = activeSession {
|
||||
PlayerHandler.stopPlayback(currentSessionId: activeSession.id)
|
||||
await PlayerProgress.shared.syncFromServer()
|
||||
try self.startPlaybackSession(activeSession, playWhenReady: false, playbackRate: PlayerSettings.main().playbackRate)
|
||||
}
|
||||
} catch {
|
||||
NSLog("Failed to restore playback session")
|
||||
logger.error("Failed to restore playback session")
|
||||
debugPrint(error)
|
||||
}
|
||||
}
|
||||
@@ -67,7 +67,7 @@ public class AbsAudioPlayer: CAPPlugin {
|
||||
let playbackRate = call.getFloat("playbackRate", 1)
|
||||
|
||||
if libraryItemId == nil {
|
||||
NSLog("provide library item id")
|
||||
logger.error("provide library item id")
|
||||
return call.resolve()
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ public class AbsAudioPlayer: CAPPlugin {
|
||||
let item = Database.shared.getLocalLibraryItem(localLibraryItemId: libraryItemId!)
|
||||
let episode = item?.getPodcastEpisode(episodeId: episodeId)
|
||||
guard let playbackSession = item?.getPlaybackSession(episode: episode) else {
|
||||
NSLog("Failed to get local playback session")
|
||||
logger.error("Failed to get local playback session")
|
||||
return call.resolve([:])
|
||||
}
|
||||
|
||||
@@ -87,18 +87,18 @@ public class AbsAudioPlayer: CAPPlugin {
|
||||
try self.startPlaybackSession(playbackSession, playWhenReady: playWhenReady, playbackRate: playbackRate)
|
||||
call.resolve(try playbackSession.asDictionary())
|
||||
} catch(let exception) {
|
||||
NSLog("Failed to start session")
|
||||
logger.error("Failed to start session")
|
||||
debugPrint(exception)
|
||||
call.resolve([:])
|
||||
}
|
||||
} else { // Playing from the server
|
||||
ApiClient.startPlaybackSession(libraryItemId: libraryItemId!, episodeId: episodeId, forceTranscode: false) { session in
|
||||
ApiClient.startPlaybackSession(libraryItemId: libraryItemId!, episodeId: episodeId, forceTranscode: false) { [weak self] session in
|
||||
do {
|
||||
try session.save()
|
||||
try self.startPlaybackSession(session, playWhenReady: playWhenReady, playbackRate: playbackRate)
|
||||
try self?.startPlaybackSession(session, playWhenReady: playWhenReady, playbackRate: playbackRate)
|
||||
call.resolve(try session.asDictionary())
|
||||
} catch(let exception) {
|
||||
NSLog("Failed to start session")
|
||||
self?.logger.error("Failed to start session")
|
||||
debugPrint(exception)
|
||||
call.resolve([:])
|
||||
}
|
||||
@@ -107,7 +107,7 @@ public class AbsAudioPlayer: CAPPlugin {
|
||||
}
|
||||
|
||||
@objc func closePlayback(_ call: CAPPluginCall) {
|
||||
NSLog("Close playback")
|
||||
logger.log("Close playback")
|
||||
|
||||
PlayerHandler.stopPlayback()
|
||||
call.resolve()
|
||||
@@ -193,7 +193,7 @@ public class AbsAudioPlayer: CAPPlugin {
|
||||
|
||||
let seconds = time / 1000
|
||||
|
||||
NSLog("chapter time: \(isChapterTime)")
|
||||
logger.log("chapter time: \(isChapterTime)")
|
||||
if isChapterTime {
|
||||
PlayerHandler.setChapterSleepTime(stopAt: seconds)
|
||||
return call.resolve([ "success": true ])
|
||||
@@ -230,7 +230,7 @@ public class AbsAudioPlayer: CAPPlugin {
|
||||
guard let localMediaProgressId = PlayerHandler.getPlaybackSession()?.localMediaProgressId else { return }
|
||||
guard let localMediaProgress = Database.shared.getLocalMediaProgress(localMediaProgressId: localMediaProgressId) else { return }
|
||||
guard let progressUpdate = try? localMediaProgress.asDictionary() else { return }
|
||||
NSLog("Sending local progress back to the UI")
|
||||
logger.log("Sending local progress back to the UI")
|
||||
self.notifyListeners("onLocalMediaProgressUpdate", data: progressUpdate)
|
||||
}
|
||||
|
||||
@@ -239,17 +239,18 @@ public class AbsAudioPlayer: CAPPlugin {
|
||||
let session = PlayerHandler.getPlaybackSession()
|
||||
let libraryItemId = session?.libraryItemId ?? ""
|
||||
let episodeId = session?.episodeId ?? nil
|
||||
NSLog("Forcing Transcode")
|
||||
logger.log("Forcing Transcode")
|
||||
|
||||
// If direct playing then fallback to transcode
|
||||
ApiClient.startPlaybackSession(libraryItemId: libraryItemId, episodeId: episodeId, forceTranscode: true) { session in
|
||||
ApiClient.startPlaybackSession(libraryItemId: libraryItemId, episodeId: episodeId, forceTranscode: true) { [weak self] session in
|
||||
do {
|
||||
guard let self = self else { return }
|
||||
try session.save()
|
||||
PlayerHandler.startPlayback(sessionId: session.id, playWhenReady: self.initialPlayWhenReady, playbackRate: PlayerSettings.main().playbackRate)
|
||||
self.sendPlaybackSession(session: try session.asDictionary())
|
||||
self.sendMetadata()
|
||||
} catch(let exception) {
|
||||
NSLog("Failed to start transcoded session")
|
||||
self?.logger.error("Failed to start transcoded session")
|
||||
debugPrint(exception)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ extension String {
|
||||
|
||||
@objc(AbsDatabase)
|
||||
public class AbsDatabase: CAPPlugin {
|
||||
private let logger = AppLogger(category: "AbsDatabase")
|
||||
|
||||
@objc func setCurrentServerConnectionConfig(_ call: CAPPluginCall) {
|
||||
var id = call.getString("id")
|
||||
let address = call.getString("address", "")
|
||||
@@ -82,7 +84,7 @@ public class AbsDatabase: CAPPlugin {
|
||||
let items = Database.shared.getLocalLibraryItems()
|
||||
call.resolve([ "value": try items.asDictionaryArray()])
|
||||
} catch(let exception) {
|
||||
NSLog("error while readling local library items")
|
||||
logger.error("error while readling local library items")
|
||||
debugPrint(exception)
|
||||
call.resolve()
|
||||
}
|
||||
@@ -98,7 +100,7 @@ public class AbsDatabase: CAPPlugin {
|
||||
call.resolve()
|
||||
}
|
||||
} catch(let exception) {
|
||||
NSLog("error while readling local library items")
|
||||
logger.error("error while readling local library items")
|
||||
debugPrint(exception)
|
||||
call.resolve()
|
||||
}
|
||||
@@ -114,7 +116,7 @@ public class AbsDatabase: CAPPlugin {
|
||||
call.resolve()
|
||||
}
|
||||
} catch(let exception) {
|
||||
NSLog("error while readling local library items")
|
||||
logger.error("error while readling local library items")
|
||||
debugPrint(exception)
|
||||
call.resolve()
|
||||
}
|
||||
@@ -128,7 +130,7 @@ public class AbsDatabase: CAPPlugin {
|
||||
do {
|
||||
call.resolve([ "value": try Database.shared.getAllLocalMediaProgress().asDictionaryArray() ])
|
||||
} catch {
|
||||
NSLog("Error while loading local media progress")
|
||||
logger.error("Error while loading local media progress")
|
||||
debugPrint(error)
|
||||
call.resolve(["value": []])
|
||||
}
|
||||
@@ -178,7 +180,7 @@ public class AbsDatabase: CAPPlugin {
|
||||
return
|
||||
}
|
||||
|
||||
NSLog("syncServerMediaProgressWithLocalMediaProgress: Saving local media progress")
|
||||
logger.log("syncServerMediaProgressWithLocalMediaProgress: Saving local media progress")
|
||||
try localMediaProgress.updateFromServerMediaProgress(serverMediaProgress)
|
||||
|
||||
call.resolve(try localMediaProgress.asDictionary())
|
||||
@@ -194,7 +196,7 @@ public class AbsDatabase: CAPPlugin {
|
||||
let localMediaProgressId = call.getString("localMediaProgressId")
|
||||
let isFinished = call.getBool("isFinished", false)
|
||||
|
||||
NSLog("updateLocalMediaProgressFinished \(localMediaProgressId ?? "Unknown") | Is Finished: \(isFinished)")
|
||||
logger.log("updateLocalMediaProgressFinished \(localMediaProgressId ?? "Unknown") | Is Finished: \(isFinished)")
|
||||
|
||||
do {
|
||||
let localMediaProgress = try LocalMediaProgress.fetchOrCreateLocalMediaProgress(localMediaProgressId: localMediaProgressId, localLibraryItemId: localLibraryItemId, localEpisodeId: localEpisodeId)
|
||||
|
||||
@@ -14,6 +14,8 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
|
||||
|
||||
static private let downloadsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||
|
||||
private let logger = AppLogger(category: "AbsDownloader")
|
||||
|
||||
private lazy var session: URLSession = {
|
||||
let config = URLSessionConfiguration.background(withIdentifier: "AbsDownloader")
|
||||
let queue = OperationQueue()
|
||||
@@ -94,7 +96,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
|
||||
private func handleDownloadTaskUpdate(downloadTask: URLSessionTask, progressHandler: DownloadProgressHandler) {
|
||||
do {
|
||||
guard let downloadItemPartId = downloadTask.taskDescription else { throw LibraryItemDownloadError.noTaskDescription }
|
||||
NSLog("Received download update for \(downloadItemPartId)")
|
||||
logger.log("Received download update for \(downloadItemPartId)")
|
||||
|
||||
// Find the download item
|
||||
let downloadItem = Database.shared.getDownloadItem(downloadItemPartId: downloadItemPartId)
|
||||
@@ -108,7 +110,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
|
||||
do {
|
||||
try progressHandler(downloadItem, part)
|
||||
} catch {
|
||||
NSLog("Error while processing progress")
|
||||
logger.error("Error while processing progress")
|
||||
debugPrint(error)
|
||||
}
|
||||
|
||||
@@ -119,7 +121,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
|
||||
}
|
||||
self.notifyDownloadProgress()
|
||||
} catch {
|
||||
NSLog("DownloadItemError")
|
||||
logger.error("DownloadItemError")
|
||||
debugPrint(error)
|
||||
}
|
||||
}
|
||||
@@ -127,18 +129,18 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
|
||||
// We want to handle updating the UI in the background and throttled so we don't overload the UI with progress updates
|
||||
private func notifyDownloadProgress() {
|
||||
if self.monitoringProgressTimer?.isValid ?? false {
|
||||
NSLog("Already monitoring progress, no need to start timer again")
|
||||
logger.log("Already monitoring progress, no need to start timer again")
|
||||
} else {
|
||||
DispatchQueue.runOnMainQueue {
|
||||
self.monitoringProgressTimer = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: true, block: { t in
|
||||
NSLog("Starting monitoring download progress...")
|
||||
self.monitoringProgressTimer = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: true, block: { [unowned self] t in
|
||||
self.logger.log("Starting monitoring download progress...")
|
||||
|
||||
// Fetch active downloads in a thread-safe way
|
||||
func fetchActiveDownloads() -> [String: DownloadItem]? {
|
||||
self.progressStatusQueue.sync {
|
||||
let activeDownloads = self.downloadItemProgress
|
||||
if activeDownloads.isEmpty {
|
||||
NSLog("Finishing monitoring download progress...")
|
||||
logger.log("Finishing monitoring download progress...")
|
||||
t.invalidate()
|
||||
}
|
||||
return activeDownloads
|
||||
@@ -173,8 +175,8 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
|
||||
statusNotification["libraryItemId"] = downloadItem.id
|
||||
|
||||
if ( downloadItem.didDownloadSuccessfully() ) {
|
||||
ApiClient.getLibraryItemWithProgress(libraryItemId: downloadItem.libraryItemId!, episodeId: downloadItem.episodeId) { libraryItem in
|
||||
guard let libraryItem = libraryItem else { NSLog("LibraryItem not found"); return }
|
||||
ApiClient.getLibraryItemWithProgress(libraryItemId: downloadItem.libraryItemId!, episodeId: downloadItem.episodeId) { [weak self] libraryItem in
|
||||
guard let libraryItem = libraryItem else { self?.logger.error("LibraryItem not found"); return }
|
||||
let localDirectory = libraryItem.id
|
||||
var coverFile: String?
|
||||
|
||||
@@ -206,7 +208,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
|
||||
statusNotification["localMediaProgress"] = try? localMediaProgress.asDictionary()
|
||||
}
|
||||
|
||||
self.notifyListeners("onItemDownloadComplete", data: statusNotification)
|
||||
self?.notifyListeners("onItemDownloadComplete", data: statusNotification)
|
||||
}
|
||||
} else {
|
||||
self.notifyListeners("onItemDownloadComplete", data: statusNotification)
|
||||
@@ -221,22 +223,22 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
|
||||
var episodeId = call.getString("episodeId")
|
||||
if ( episodeId == "null" ) { episodeId = nil }
|
||||
|
||||
NSLog("Download library item \(libraryItemId ?? "N/A") / episode \(episodeId ?? "N/A")")
|
||||
logger.log("Download library item \(libraryItemId ?? "N/A") / episode \(episodeId ?? "N/A")")
|
||||
guard let libraryItemId = libraryItemId else { return call.resolve(["error": "libraryItemId not specified"]) }
|
||||
|
||||
ApiClient.getLibraryItemWithProgress(libraryItemId: libraryItemId, episodeId: episodeId) { libraryItem in
|
||||
ApiClient.getLibraryItemWithProgress(libraryItemId: libraryItemId, episodeId: episodeId) { [weak self] libraryItem in
|
||||
if let libraryItem = libraryItem {
|
||||
NSLog("Got library item from server \(libraryItem.id)")
|
||||
self?.logger.log("Got library item from server \(libraryItem.id)")
|
||||
do {
|
||||
if let episodeId = episodeId {
|
||||
// Download a podcast episode
|
||||
guard libraryItem.mediaType == "podcast" else { throw LibraryItemDownloadError.libraryItemNotPodcast }
|
||||
let episode = libraryItem.media?.episodes.enumerated().first(where: { $1.id == episodeId })?.element
|
||||
guard let episode = episode else { throw LibraryItemDownloadError.podcastEpisodeNotFound }
|
||||
try self.startLibraryItemDownload(libraryItem, episode: episode)
|
||||
try self?.startLibraryItemDownload(libraryItem, episode: episode)
|
||||
} else {
|
||||
// Download a book
|
||||
try self.startLibraryItemDownload(libraryItem)
|
||||
try self?.startLibraryItemDownload(libraryItem)
|
||||
}
|
||||
call.resolve()
|
||||
} catch {
|
||||
@@ -298,7 +300,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
|
||||
}
|
||||
|
||||
private func startLibraryItemTrackDownload(item: LibraryItem, position: Int, track: AudioTrack, episode: PodcastEpisode?) throws -> DownloadItemPartTask {
|
||||
NSLog("TRACK \(track.contentUrl!)")
|
||||
logger.log("TRACK \(track.contentUrl!)")
|
||||
|
||||
// If we don't name metadata, then we can't proceed
|
||||
guard let filename = track.metadata?.filename else {
|
||||
@@ -342,10 +344,10 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
|
||||
|
||||
private func createLibraryItemFileDirectory(item: LibraryItem) throws -> String {
|
||||
let itemDirectory = item.id
|
||||
NSLog("ITEM DIR \(itemDirectory)")
|
||||
logger.log("ITEM DIR \(itemDirectory)")
|
||||
|
||||
guard AbsDownloader.itemDownloadFolder(path: itemDirectory) != nil else {
|
||||
NSLog("Failed to CREATE LI DIRECTORY \(itemDirectory)")
|
||||
logger.error("Failed to CREATE LI DIRECTORY \(itemDirectory)")
|
||||
throw LibraryItemDownloadError.failedDirectory
|
||||
}
|
||||
|
||||
@@ -367,7 +369,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
|
||||
|
||||
return itemFolder
|
||||
} catch {
|
||||
NSLog("Failed to CREATE LI DIRECTORY \(error)")
|
||||
AppLogger().error("Failed to CREATE LI DIRECTORY \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,12 @@ import Capacitor
|
||||
|
||||
@objc(AbsFileSystem)
|
||||
public class AbsFileSystem: CAPPlugin {
|
||||
private let logger = AppLogger(category: "AbsFileSystem")
|
||||
|
||||
@objc func selectFolder(_ call: CAPPluginCall) {
|
||||
let mediaType = call.getString("mediaType")
|
||||
|
||||
NSLog("Select Folder for media type \(mediaType ?? "UNSET")")
|
||||
logger.log("Select Folder for media type \(mediaType ?? "UNSET")")
|
||||
|
||||
call.unavailable("Not available on iOS")
|
||||
}
|
||||
@@ -21,7 +23,7 @@ public class AbsFileSystem: CAPPlugin {
|
||||
@objc func checkFolderPermission(_ call: CAPPluginCall) {
|
||||
let folderUrl = call.getString("folderUrl")
|
||||
|
||||
NSLog("checkFolderPermission for folder \(folderUrl ?? "UNSET")")
|
||||
logger.log("checkFolderPermission for folder \(folderUrl ?? "UNSET")")
|
||||
|
||||
call.unavailable("Not available on iOS")
|
||||
}
|
||||
@@ -30,7 +32,7 @@ public class AbsFileSystem: CAPPlugin {
|
||||
let folderId = call.getString("folderId")
|
||||
let forceAudioProbe = call.getBool("forceAudioProbe", false)
|
||||
|
||||
NSLog("scanFolder \(folderId ?? "UNSET") | Force Probe = \(forceAudioProbe)")
|
||||
logger.log("scanFolder \(folderId ?? "UNSET") | Force Probe = \(forceAudioProbe)")
|
||||
|
||||
call.unavailable("Not available on iOS")
|
||||
}
|
||||
@@ -38,7 +40,7 @@ public class AbsFileSystem: CAPPlugin {
|
||||
@objc func removeFolder(_ call: CAPPluginCall) {
|
||||
let folderId = call.getString("folderId")
|
||||
|
||||
NSLog("removeFolder \(folderId ?? "UNSET")")
|
||||
logger.log("removeFolder \(folderId ?? "UNSET")")
|
||||
|
||||
call.unavailable("Not available on iOS")
|
||||
}
|
||||
@@ -46,7 +48,7 @@ public class AbsFileSystem: CAPPlugin {
|
||||
@objc func removeLocalLibraryItem(_ call: CAPPluginCall) {
|
||||
let localLibraryItemId = call.getString("localLibraryItemId")
|
||||
|
||||
NSLog("removeLocalLibraryItem \(localLibraryItemId ?? "UNSET")")
|
||||
logger.log("removeLocalLibraryItem \(localLibraryItemId ?? "UNSET")")
|
||||
|
||||
call.unavailable("Not available on iOS")
|
||||
}
|
||||
@@ -55,7 +57,7 @@ public class AbsFileSystem: CAPPlugin {
|
||||
let localLibraryItemId = call.getString("localLibraryItemId")
|
||||
let forceAudioProbe = call.getBool("forceAudioProbe", false)
|
||||
|
||||
NSLog("scanLocalLibraryItem \(localLibraryItemId ?? "UNSET") | Force Probe = \(forceAudioProbe)")
|
||||
logger.log("scanLocalLibraryItem \(localLibraryItemId ?? "UNSET") | Force Probe = \(forceAudioProbe)")
|
||||
|
||||
call.unavailable("Not available on iOS")
|
||||
}
|
||||
@@ -64,7 +66,7 @@ public class AbsFileSystem: CAPPlugin {
|
||||
let localLibraryItemId = call.getString("id")
|
||||
let contentUrl = call.getString("contentUrl")
|
||||
|
||||
NSLog("deleteItem \(localLibraryItemId ?? "UNSET") url \(contentUrl ?? "UNSET")")
|
||||
logger.log("deleteItem \(localLibraryItemId ?? "UNSET") url \(contentUrl ?? "UNSET")")
|
||||
|
||||
var success = false
|
||||
do {
|
||||
@@ -74,7 +76,7 @@ public class AbsFileSystem: CAPPlugin {
|
||||
success = true
|
||||
}
|
||||
} catch {
|
||||
NSLog("Failed to delete \(error)")
|
||||
logger.error("Failed to delete \(error)")
|
||||
success = false
|
||||
}
|
||||
|
||||
@@ -85,7 +87,7 @@ public class AbsFileSystem: CAPPlugin {
|
||||
let localLibraryItemId = call.getString("id")
|
||||
let trackLocalFileId = call.getString("trackLocalFileId")
|
||||
|
||||
NSLog("deleteTrackFromItem \(localLibraryItemId ?? "UNSET") track file \(trackLocalFileId ?? "UNSET")")
|
||||
logger.log("deleteTrackFromItem \(localLibraryItemId ?? "UNSET") track file \(trackLocalFileId ?? "UNSET")")
|
||||
|
||||
var success = false
|
||||
if let localLibraryItemId = localLibraryItemId, let trackLocalFileId = trackLocalFileId, let item = Database.shared.getLocalLibraryItem(localLibraryItemId: localLibraryItemId) {
|
||||
@@ -105,12 +107,12 @@ public class AbsFileSystem: CAPPlugin {
|
||||
success = true
|
||||
}
|
||||
} catch {
|
||||
NSLog("Failed to delete \(error)")
|
||||
logger.error("Failed to delete \(error)")
|
||||
success = false
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
NSLog("Failed to delete \(error)")
|
||||
logger.error("Failed to delete \(error)")
|
||||
success = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// PlayerTimeUtilsTests.swift
|
||||
// AudiobookshelfUnitTests
|
||||
//
|
||||
// Created by Ron Heft on 9/20/22.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Audiobookshelf
|
||||
|
||||
final class PlayerTimeUtilsTests: XCTestCase {
|
||||
|
||||
func testCalcSeekBackTime() {
|
||||
let currentTime: Double = 1000
|
||||
let threeSecondsAgo = Date(timeIntervalSinceNow: -3)
|
||||
let lastPlayedMs = threeSecondsAgo.timeIntervalSince1970 * 1000
|
||||
XCTAssertEqual(PlayerTimeUtils.calcSeekBackTime(currentTime: currentTime, lastPlayedMs: lastPlayedMs), 998)
|
||||
}
|
||||
|
||||
func testCalcSeekBackTimeWithZeroCurrentTime() {
|
||||
let currentTime: Double = 0
|
||||
let threeHundredSecondsAgo = Date(timeIntervalSinceNow: -300)
|
||||
let lastPlayedMs = threeHundredSecondsAgo.timeIntervalSince1970 * 1000
|
||||
XCTAssertEqual(PlayerTimeUtils.calcSeekBackTime(currentTime: currentTime, lastPlayedMs: lastPlayedMs), 0)
|
||||
}
|
||||
|
||||
func testTimeSinceLastPlayed() throws {
|
||||
let fiveSecondsAgo = Date(timeIntervalSinceNow: -5)
|
||||
let lastPlayedMs = fiveSecondsAgo.timeIntervalSince1970 * 1000
|
||||
XCTAssertEqual(PlayerTimeUtils.timeSinceLastPlayed(lastPlayedMs)!, -5, accuracy: 1.0)
|
||||
XCTAssertNil(PlayerTimeUtils.timeSinceLastPlayed(nil))
|
||||
}
|
||||
|
||||
func testTimeToSeekBackForSinceLastPlayed() throws {
|
||||
XCTAssertEqual(PlayerTimeUtils.timeToSeekBackForSinceLastPlayed(nil), 5, "Seeks back 5 seconds for nil")
|
||||
XCTAssertEqual(PlayerTimeUtils.timeToSeekBackForSinceLastPlayed(5), 2, "Seeks back 2 seconds for less than 6 seconds")
|
||||
XCTAssertEqual(PlayerTimeUtils.timeToSeekBackForSinceLastPlayed(11), 10, "Seeks back 10 seconds for less than 12 seconds")
|
||||
XCTAssertEqual(PlayerTimeUtils.timeToSeekBackForSinceLastPlayed(29), 15, "Seeks back 15 seconds for less than 30 seconds")
|
||||
XCTAssertEqual(PlayerTimeUtils.timeToSeekBackForSinceLastPlayed(179), 20, "Seeks back 20 seconds for less than 2 minutes")
|
||||
XCTAssertEqual(PlayerTimeUtils.timeToSeekBackForSinceLastPlayed(3599), 25, "Seeks back 25 seconds for less than 59 minutes")
|
||||
XCTAssertEqual(PlayerTimeUtils.timeToSeekBackForSinceLastPlayed(60000), 29, "Seeks back 29 seconds for anything over 59 minuts")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,7 +17,7 @@ def capacitor_pods
|
||||
pod 'CapacitorStorage', :path => '../../node_modules/@capacitor/storage'
|
||||
end
|
||||
|
||||
target 'App' do
|
||||
target 'Audiobookshelf' do
|
||||
capacitor_pods
|
||||
# Add your Pods here
|
||||
|
||||
|
||||
@@ -39,14 +39,18 @@ class LocalLibraryItem: Object, Codable {
|
||||
}
|
||||
}
|
||||
|
||||
var coverContentUrl: String? {
|
||||
var coverUrl: URL? {
|
||||
if let path = self._coverContentUrl {
|
||||
return AbsDownloader.itemDownloadFolder(path: path)!.absoluteString
|
||||
return AbsDownloader.itemDownloadFolder(path: path)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var coverContentUrl: String? {
|
||||
return self.coverUrl?.absoluteString
|
||||
}
|
||||
|
||||
var isBook: Bool { self.mediaType == "book" }
|
||||
var isPodcast: Bool { self.mediaType == "podcast" }
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ enum PlayMethod:Int {
|
||||
|
||||
class AudioPlayer: NSObject {
|
||||
internal let queue = DispatchQueue(label: "ABSAudioPlayerQueue")
|
||||
internal let logger = AppLogger(category: "AudioPlayer")
|
||||
|
||||
// enums and @objc are not compatible
|
||||
@objc dynamic var status: Int
|
||||
@@ -68,7 +69,7 @@ class AudioPlayer: NSObject {
|
||||
|
||||
let playbackSession = self.getPlaybackSession()
|
||||
guard let playbackSession = playbackSession else {
|
||||
NSLog("Failed to fetch playback session. Player will not initialize")
|
||||
logger.error("Failed to fetch playback session. Player will not initialize")
|
||||
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil)
|
||||
return
|
||||
}
|
||||
@@ -86,10 +87,10 @@ class AudioPlayer: NSObject {
|
||||
}
|
||||
|
||||
self.currentTrackIndex = getItemIndexForTime(time: playbackSession.currentTime)
|
||||
NSLog("Starting track index \(self.currentTrackIndex) for start time \(playbackSession.currentTime)")
|
||||
logger.log("Starting track index \(self.currentTrackIndex) for start time \(playbackSession.currentTime)")
|
||||
|
||||
let playerItems = self.allPlayerItems[self.currentTrackIndex..<self.allPlayerItems.count]
|
||||
NSLog("Setting player items \(playerItems.count)")
|
||||
logger.log("Setting player items \(playerItems.count)")
|
||||
|
||||
for item in Array(playerItems) {
|
||||
self.audioPlayer.insert(item, after:self.audioPlayer.items().last)
|
||||
@@ -99,13 +100,10 @@ class AudioPlayer: NSObject {
|
||||
setupQueueObserver()
|
||||
setupQueueItemStatusObserver()
|
||||
|
||||
NSLog("Audioplayer ready")
|
||||
logger.log("Audioplayer ready")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.stopPausedTimer()
|
||||
self.removeSleepTimer()
|
||||
self.removeTimeObserver()
|
||||
self.queueObserver?.invalidate()
|
||||
self.queueItemStatusObserver?.invalidate()
|
||||
}
|
||||
@@ -120,8 +118,8 @@ class AudioPlayer: NSObject {
|
||||
do {
|
||||
try AVAudioSession.sharedInstance().setActive(false)
|
||||
} catch {
|
||||
NSLog("Failed to set AVAudioSession inactive")
|
||||
print(error)
|
||||
logger.error("Failed to set AVAudioSession inactive")
|
||||
logger.error(error)
|
||||
}
|
||||
|
||||
self.removeAudioSessionNotifications()
|
||||
@@ -132,8 +130,13 @@ class AudioPlayer: NSObject {
|
||||
// Remove observers
|
||||
self.audioPlayer.removeObserver(self, forKeyPath: #keyPath(AVPlayer.rate), context: &playerContext)
|
||||
self.audioPlayer.removeObserver(self, forKeyPath: #keyPath(AVPlayer.currentItem), context: &playerContext)
|
||||
self.removeTimeObserver()
|
||||
|
||||
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.closed.rawValue), object: nil)
|
||||
|
||||
// Remove timers
|
||||
self.stopPausedTimer()
|
||||
self.removeSleepTimer()
|
||||
}
|
||||
|
||||
public func isInitialized() -> Bool {
|
||||
@@ -178,6 +181,7 @@ class AudioPlayer: NSObject {
|
||||
let time = CMTime(seconds: Double(seconds), preferredTimescale: timeScale)
|
||||
self.timeObserverToken = self.audioPlayer.addPeriodicTimeObserver(forInterval: time, queue: self.queue) { [weak self] time in
|
||||
guard let self = self else { return }
|
||||
guard self.isInitialized() else { return }
|
||||
|
||||
guard let currentTime = self.getCurrentTime() else { return }
|
||||
let isPlaying = self.isPlaying()
|
||||
@@ -190,11 +194,6 @@ class AudioPlayer: NSObject {
|
||||
if self.isSleepTimerSet() {
|
||||
// Update the UI
|
||||
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil)
|
||||
|
||||
// Handle a sitation where the user skips past the chapter end
|
||||
if self.isChapterSleepTimerBeforeTime(currentTime) {
|
||||
self.removeSleepTimer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -214,14 +213,14 @@ class AudioPlayer: NSObject {
|
||||
self.audioPlayer.currentItem.map { item in
|
||||
self.currentTrackIndex = self.allPlayerItems.firstIndex(of:item) ?? 0
|
||||
if (self.currentTrackIndex != prevTrackIndex) {
|
||||
NSLog("New Current track index \(self.currentTrackIndex)")
|
||||
self.logger.log("New Current track index \(self.currentTrackIndex)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setupQueueItemStatusObserver() {
|
||||
NSLog("queueStatusObserver: Setting up")
|
||||
logger.log("queueStatusObserver: Setting up")
|
||||
|
||||
// Listen for player item updates
|
||||
self.queueItemStatusObserver?.invalidate()
|
||||
@@ -236,13 +235,13 @@ class AudioPlayer: NSObject {
|
||||
}
|
||||
|
||||
private func handleQueueItemStatus(playerItem: AVPlayerItem) {
|
||||
NSLog("queueStatusObserver: Current item status changed")
|
||||
logger.log("queueStatusObserver: Current item status changed")
|
||||
guard let playbackSession = self.getPlaybackSession() else {
|
||||
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil)
|
||||
return
|
||||
}
|
||||
if (playerItem.status == .readyToPlay) {
|
||||
NSLog("queueStatusObserver: Current Item Ready to play. PlayWhenReady: \(self.playWhenReady)")
|
||||
logger.log("queueStatusObserver: Current Item Ready to play. PlayWhenReady: \(self.playWhenReady)")
|
||||
|
||||
// Seek the player before initializing, so a currentTime of 0 does not appear in MediaProgress / session
|
||||
let firstReady = self.status < 0
|
||||
@@ -252,16 +251,16 @@ class AudioPlayer: NSObject {
|
||||
self.seek(playbackSession.currentTime, from: "queueItemStatusObserver")
|
||||
}
|
||||
|
||||
// Mark the player as ready
|
||||
self.status = 0
|
||||
|
||||
// Start the player, if requested
|
||||
if self.playWhenReady {
|
||||
self.playWhenReady = false
|
||||
self.play()
|
||||
self.play(allowSeekBack: false, isInitializing: true)
|
||||
} else {
|
||||
// Mark the player as ready
|
||||
self.status = 0
|
||||
}
|
||||
} else if (playerItem.status == .failed) {
|
||||
NSLog("queueStatusObserver: FAILED \(playerItem.error?.localizedDescription ?? "")")
|
||||
logger.error("queueStatusObserver: FAILED \(playerItem.error?.localizedDescription ?? "")")
|
||||
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil)
|
||||
}
|
||||
}
|
||||
@@ -269,8 +268,8 @@ class AudioPlayer: NSObject {
|
||||
private func startPausedTimer() {
|
||||
guard self.pausedTimer == nil else { return }
|
||||
self.queue.async {
|
||||
self.pausedTimer = Timer.scheduledTimer(withTimeInterval: 10, repeats: true) { timer in
|
||||
NSLog("PAUSE TIMER: Syncing from server")
|
||||
self.pausedTimer = Timer.scheduledTimer(withTimeInterval: 10, repeats: true) { [weak self] timer in
|
||||
self?.logger.log("PAUSE TIMER: Syncing from server")
|
||||
Task { await PlayerProgress.shared.syncFromServer() }
|
||||
}
|
||||
}
|
||||
@@ -282,16 +281,15 @@ class AudioPlayer: NSObject {
|
||||
}
|
||||
|
||||
// MARK: - Methods
|
||||
public func play(allowSeekBack: Bool = false) {
|
||||
guard self.isInitialized() else { return }
|
||||
public func play(allowSeekBack: Bool = false, isInitializing: Bool = false) {
|
||||
guard self.isInitialized() || isInitializing else { return }
|
||||
guard let session = self.getPlaybackSession() else {
|
||||
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Determine where we are starting playback
|
||||
let lastPlayed = (session.updatedAt ?? 0)/1000
|
||||
let currentTime = allowSeekBack ? calculateSeekBackTimeAtCurrentTime(session.currentTime, lastPlayed: lastPlayed) : session.currentTime
|
||||
let currentTime = allowSeekBack ? PlayerTimeUtils.calcSeekBackTime(currentTime: session.currentTime, lastPlayedMs: session.updatedAt) : session.currentTime
|
||||
|
||||
// Sync our new playback position
|
||||
Task { await PlayerProgress.shared.syncFromPlayer(currentTime: currentTime, includesPlayProgress: self.isPlaying(), isStopping: false) }
|
||||
@@ -299,57 +297,42 @@ class AudioPlayer: NSObject {
|
||||
// Start playback, with a seek, for as smooth a scrub bar start as possible
|
||||
let currentTrackStartOffset = session.audioTracks[self.currentTrackIndex].startOffset ?? 0.0
|
||||
let seekTime = currentTime - currentTrackStartOffset
|
||||
self.audioPlayer.seek(to: CMTime(seconds: seekTime, preferredTimescale: 1000), toleranceBefore: .zero, toleranceAfter: .zero) { [weak self] completed in
|
||||
guard completed else { return }
|
||||
self?.resumePlayback()
|
||||
}
|
||||
}
|
||||
|
||||
private func calculateSeekBackTimeAtCurrentTime(_ currentTime: Double, lastPlayed: Double) -> Double {
|
||||
let difference = Date.timeIntervalSinceReferenceDate - lastPlayed
|
||||
var time: Double = 0
|
||||
|
||||
// Scale seek back time based on how long since last play
|
||||
if lastPlayed == 0 {
|
||||
time = 5
|
||||
} else if difference < 6 {
|
||||
time = 2
|
||||
} else if difference < 12 {
|
||||
time = 10
|
||||
} else if difference < 30 {
|
||||
time = 15
|
||||
} else if difference < 180 {
|
||||
time = 20
|
||||
} else if difference < 3600 {
|
||||
time = 25
|
||||
} else {
|
||||
time = 29
|
||||
DispatchQueue.runOnMainQueue {
|
||||
self.audioPlayer.seek(to: CMTime(seconds: seekTime, preferredTimescale: 1000), toleranceBefore: .zero, toleranceAfter: .zero) { [weak self] completed in
|
||||
guard completed else { return }
|
||||
self?.resumePlayback()
|
||||
}
|
||||
}
|
||||
|
||||
// Wind the clock back
|
||||
return currentTime - time
|
||||
}
|
||||
|
||||
private func resumePlayback() {
|
||||
NSLog("PLAY: Resuming playback")
|
||||
logger.log("PLAY: Resuming playback")
|
||||
|
||||
// Stop the paused timer
|
||||
self.stopPausedTimer()
|
||||
|
||||
self.markAudioSessionAs(active: true)
|
||||
self.audioPlayer.play()
|
||||
self.audioPlayer.rate = self.tmpRate
|
||||
DispatchQueue.runOnMainQueue {
|
||||
self.audioPlayer.play()
|
||||
self.audioPlayer.rate = self.tmpRate
|
||||
}
|
||||
self.status = 1
|
||||
|
||||
// Update the progress
|
||||
self.updateNowPlaying()
|
||||
|
||||
// Handle a chapter sleep timer that may now be invalid
|
||||
self.handleTrackChangeForChapterSleepTimer()
|
||||
}
|
||||
|
||||
public func pause() {
|
||||
guard self.isInitialized() else { return }
|
||||
|
||||
NSLog("PAUSE: Pausing playback")
|
||||
self.audioPlayer.pause()
|
||||
logger.log("PAUSE: Pausing playback")
|
||||
DispatchQueue.runOnMainQueue {
|
||||
self.audioPlayer.pause()
|
||||
}
|
||||
self.markAudioSessionAs(active: false)
|
||||
|
||||
Task {
|
||||
@@ -370,17 +353,17 @@ class AudioPlayer: NSObject {
|
||||
|
||||
self.pause()
|
||||
|
||||
NSLog("SEEK: Seek to \(to) from \(from)")
|
||||
logger.log("SEEK: Seek to \(to) from \(from) and continuePlaying(\(continuePlaying)")
|
||||
|
||||
guard let playbackSession = self.getPlaybackSession() else { return }
|
||||
|
||||
let currentTrack = playbackSession.audioTracks[self.currentTrackIndex]
|
||||
let ctso = currentTrack.startOffset ?? 0.0
|
||||
let trackEnd = ctso + currentTrack.duration
|
||||
NSLog("SEEK: Seek current track END = \(trackEnd)")
|
||||
logger.log("SEEK: Seek current track END = \(trackEnd)")
|
||||
|
||||
let indexOfSeek = getItemIndexForTime(time: to)
|
||||
NSLog("SEEK: Seek to index \(indexOfSeek) | Current index \(self.currentTrackIndex)")
|
||||
logger.log("SEEK: Seek to index \(indexOfSeek) | Current index \(self.currentTrackIndex)")
|
||||
|
||||
// Reconstruct queue if seeking to a different track
|
||||
if (self.currentTrackIndex != indexOfSeek) {
|
||||
@@ -394,26 +377,34 @@ class AudioPlayer: NSObject {
|
||||
self.status = -1
|
||||
let playerItems = self.allPlayerItems[indexOfSeek..<self.allPlayerItems.count]
|
||||
|
||||
self.audioPlayer.removeAllItems()
|
||||
for item in Array(playerItems) {
|
||||
self.audioPlayer.insert(item, after:self.audioPlayer.items().last)
|
||||
DispatchQueue.runOnMainQueue {
|
||||
self.audioPlayer.removeAllItems()
|
||||
for item in Array(playerItems) {
|
||||
self.audioPlayer.insert(item, after:self.audioPlayer.items().last)
|
||||
}
|
||||
}
|
||||
|
||||
setupQueueItemStatusObserver()
|
||||
} else {
|
||||
NSLog("SEEK: Seeking in current item \(to)")
|
||||
logger.log("SEEK: Seeking in current item \(to)")
|
||||
let currentTrackStartOffset = playbackSession.audioTracks[self.currentTrackIndex].startOffset ?? 0.0
|
||||
let seekTime = to - currentTrackStartOffset
|
||||
|
||||
self.audioPlayer.seek(to: CMTime(seconds: seekTime, preferredTimescale: 1000)) { [weak self] completed in
|
||||
guard completed else { return NSLog("SEEK: WARNING: seeking not completed (to \(seekTime)") }
|
||||
guard let self = self else { return }
|
||||
|
||||
if continuePlaying {
|
||||
self.resumePlayback()
|
||||
DispatchQueue.runOnMainQueue {
|
||||
self.audioPlayer.seek(to: CMTime(seconds: seekTime, preferredTimescale: 1000)) { [weak self] completed in
|
||||
self?.logger.log("SEEK: Completion handler called and continuePlaying(\(continuePlaying)")
|
||||
guard completed else {
|
||||
self?.logger.log("SEEK: WARNING: seeking not completed (to \(seekTime)")
|
||||
return
|
||||
}
|
||||
guard let self = self else { return }
|
||||
|
||||
if continuePlaying {
|
||||
self.resumePlayback()
|
||||
}
|
||||
|
||||
self.updateNowPlaying()
|
||||
}
|
||||
|
||||
self.updateNowPlaying()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -422,8 +413,10 @@ class AudioPlayer: NSObject {
|
||||
let playbackSpeedChanged = rate > 0.0 && rate != self.tmpRate && !(observed && rate == 1)
|
||||
|
||||
if self.audioPlayer.rate != rate {
|
||||
NSLog("setPlaybakRate rate changed from \(self.audioPlayer.rate) to \(rate)")
|
||||
self.audioPlayer.rate = rate
|
||||
logger.log("setPlaybakRate rate changed from \(self.audioPlayer.rate) to \(rate)")
|
||||
DispatchQueue.runOnMainQueue {
|
||||
self.audioPlayer.rate = rate
|
||||
}
|
||||
}
|
||||
|
||||
self.rate = rate
|
||||
@@ -477,7 +470,7 @@ class AudioPlayer: NSObject {
|
||||
} else if (playbackSession.playMethod == PlayMethod.local.rawValue) {
|
||||
guard let localFile = track.getLocalFile() else {
|
||||
// Worst case we can stream the file
|
||||
NSLog("Unable to play local file. Resulting to streaming \(track.localFileId ?? "Unknown")")
|
||||
logger.log("Unable to play local file. Resulting to streaming \(track.localFileId ?? "Unknown")")
|
||||
let filename = track.metadata?.filename ?? ""
|
||||
let filenameEncoded = filename.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed)
|
||||
let urlstr = "\(Store.serverConfig!.address)/s/item/\(itemId)/\(filenameEncoded ?? "")?token=\(Store.serverConfig!.token)"
|
||||
@@ -497,8 +490,8 @@ class AudioPlayer: NSObject {
|
||||
do {
|
||||
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .spokenAudio)
|
||||
} catch {
|
||||
NSLog("Failed to set AVAudioSession category")
|
||||
print(error)
|
||||
logger.error("Failed to set AVAudioSession category")
|
||||
logger.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -506,7 +499,7 @@ class AudioPlayer: NSObject {
|
||||
do {
|
||||
try AVAudioSession.sharedInstance().setActive(active)
|
||||
} catch {
|
||||
NSLog("Failed to set audio session as active=\(active)")
|
||||
logger.error("Failed to set audio session as active=\(active)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -637,11 +630,11 @@ class AudioPlayer: NSObject {
|
||||
public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||
if context == &playerContext {
|
||||
if keyPath == #keyPath(AVPlayer.rate) {
|
||||
NSLog("playerContext observer player rate")
|
||||
logger.log("playerContext observer player rate")
|
||||
self.setPlaybackRate(change?[.newKey] as? Float ?? 1.0, observed: true)
|
||||
} else if keyPath == #keyPath(AVPlayer.currentItem) {
|
||||
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.update.rawValue), object: nil)
|
||||
NSLog("WARNING: Item ended")
|
||||
logger.log("WARNING: Item ended")
|
||||
}
|
||||
} else {
|
||||
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
|
||||
|
||||
@@ -31,7 +31,7 @@ extension AudioPlayer {
|
||||
}
|
||||
|
||||
public func setSleepTimer(secondsUntilSleep: Double) {
|
||||
NSLog("SLEEP TIMER: Sleeping in \(secondsUntilSleep) seconds")
|
||||
logger.log("SLEEP TIMER: Sleeping in \(secondsUntilSleep) seconds")
|
||||
self.removeSleepTimer()
|
||||
self.sleepTimeRemaining = secondsUntilSleep
|
||||
|
||||
@@ -47,9 +47,13 @@ extension AudioPlayer {
|
||||
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil)
|
||||
}
|
||||
|
||||
public func setChapterSleepTimer(stopAt: Double) {
|
||||
NSLog("SLEEP TIMER: Scheduling for chapter end \(stopAt)")
|
||||
public func setChapterSleepTimer(stopAt: Double?) {
|
||||
self.removeSleepTimer()
|
||||
guard let stopAt = stopAt else { return }
|
||||
guard let currentTime = self.getCurrentTime() else { return }
|
||||
guard stopAt >= currentTime else { return }
|
||||
|
||||
logger.log("SLEEP TIMER: Scheduling for chapter end \(stopAt)")
|
||||
|
||||
// Schedule the observation time
|
||||
self.sleepTimeChapterStopAt = stopAt
|
||||
@@ -111,7 +115,12 @@ extension AudioPlayer {
|
||||
|
||||
// MARK: - Internal helpers
|
||||
|
||||
internal func decrementSleepTimerIfRunning() {
|
||||
internal func handleTrackChangeForChapterSleepTimer() {
|
||||
// If no sleep timer is set, this does nothing
|
||||
self.setChapterSleepTimer(stopAt: self.sleepTimeChapterStopAt)
|
||||
}
|
||||
|
||||
private func decrementSleepTimerIfRunning() {
|
||||
if var sleepTimeRemaining = self.sleepTimeRemaining {
|
||||
sleepTimeRemaining -= 1
|
||||
self.sleepTimeRemaining = sleepTimeRemaining
|
||||
@@ -124,7 +133,7 @@ extension AudioPlayer {
|
||||
}
|
||||
|
||||
private func handleSleepEnd() {
|
||||
NSLog("SLEEP TIMER: Pausing audio")
|
||||
logger.log("SLEEP TIMER: Pausing audio")
|
||||
self.pause()
|
||||
self.removeSleepTimer()
|
||||
}
|
||||
@@ -137,19 +146,11 @@ extension AudioPlayer {
|
||||
self.sleepTimeChapterStopAt = nil
|
||||
}
|
||||
|
||||
internal func isChapterSleepTimerBeforeTime(_ time: Double) -> Bool {
|
||||
if let chapterStopAt = self.sleepTimeChapterStopAt {
|
||||
return chapterStopAt <= time
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
internal func isCountdownSleepTimerSet() -> Bool {
|
||||
private func isCountdownSleepTimerSet() -> Bool {
|
||||
return self.sleepTimeRemaining != nil
|
||||
}
|
||||
|
||||
internal func isChapterSleepTimerSet() -> Bool {
|
||||
private func isChapterSleepTimerSet() -> Bool {
|
||||
return self.sleepTimeChapterStopAt != nil
|
||||
}
|
||||
|
||||
|
||||
@@ -15,31 +15,24 @@ class PlayerHandler {
|
||||
guard let session = Database.shared.getPlaybackSession(id: sessionId) else { return }
|
||||
|
||||
// Clean up the existing player
|
||||
if player != nil {
|
||||
player?.destroy()
|
||||
player = nil
|
||||
}
|
||||
resetPlayer()
|
||||
|
||||
// Cleanup and sync old sessions
|
||||
cleanupOldSessions(currentSessionId: sessionId)
|
||||
Task { await PlayerProgress.shared.syncToServer() }
|
||||
|
||||
// Set now playing info
|
||||
NowPlayingInfo.shared.setSessionMetadata(metadata: NowPlayingMetadata(id: session.id, itemId: session.libraryItemId!, artworkUrl: session.coverPath, title: session.displayTitle ?? "Unknown title", author: session.displayAuthor, series: nil))
|
||||
NowPlayingInfo.shared.setSessionMetadata(metadata: NowPlayingMetadata(id: session.id, itemId: session.libraryItemId!, title: session.displayTitle ?? "Unknown title", author: session.displayAuthor, series: nil))
|
||||
|
||||
// Create the audio player
|
||||
player = AudioPlayer(sessionId: sessionId, playWhenReady: playWhenReady, playbackRate: playbackRate)
|
||||
}
|
||||
|
||||
public static func stopPlayback() {
|
||||
public static func stopPlayback(currentSessionId: String? = nil) {
|
||||
// Pause playback first, so we can sync our current progress
|
||||
player?.pause()
|
||||
|
||||
player?.destroy()
|
||||
player = nil
|
||||
|
||||
cleanupOldSessions(currentSessionId: nil)
|
||||
|
||||
resetPlayer()
|
||||
cleanupOldSessions(currentSessionId: currentSessionId)
|
||||
NowPlayingInfo.shared.reset()
|
||||
}
|
||||
|
||||
@@ -52,7 +45,7 @@ class PlayerHandler {
|
||||
if paused {
|
||||
self.player?.pause()
|
||||
} else {
|
||||
self.player?.play()
|
||||
self.player?.play(allowSeekBack: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -157,4 +150,9 @@ class PlayerHandler {
|
||||
debugPrint(error)
|
||||
}
|
||||
}
|
||||
|
||||
private static func resetPlayer() {
|
||||
player?.destroy()
|
||||
player = nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ class PlayerProgress {
|
||||
|
||||
private static let TIME_BETWEEN_SESSION_SYNC_IN_SECONDS = 10.0
|
||||
|
||||
private let logger = AppLogger(category: "PlayerProgress")
|
||||
|
||||
private init() {}
|
||||
|
||||
|
||||
@@ -28,8 +30,8 @@ class PlayerProgress {
|
||||
try await updateServerSessionFromLocalSession(session, rateLimitSync: !isStopping)
|
||||
}
|
||||
} catch {
|
||||
debugPrint("Failed to syncFromPlayer")
|
||||
debugPrint(error)
|
||||
logger.error("Failed to syncFromPlayer")
|
||||
logger.error(error)
|
||||
}
|
||||
await UIApplication.shared.endBackgroundTask(backgroundToken)
|
||||
}
|
||||
@@ -39,8 +41,8 @@ class PlayerProgress {
|
||||
do {
|
||||
try await updateAllServerSessionFromLocalSession()
|
||||
} catch {
|
||||
debugPrint("Failed to syncToServer")
|
||||
debugPrint(error)
|
||||
logger.error("Failed to syncToServer")
|
||||
logger.error(error)
|
||||
}
|
||||
await UIApplication.shared.endBackgroundTask(backgroundToken)
|
||||
}
|
||||
@@ -50,8 +52,8 @@ class PlayerProgress {
|
||||
do {
|
||||
try await updateLocalSessionFromServerMediaProgress()
|
||||
} catch {
|
||||
debugPrint("Failed to syncFromServer")
|
||||
debugPrint(error)
|
||||
logger.error("Failed to syncFromServer")
|
||||
logger.error(error)
|
||||
}
|
||||
await UIApplication.shared.endBackgroundTask(backgroundToken)
|
||||
}
|
||||
@@ -96,7 +98,7 @@ class PlayerProgress {
|
||||
|
||||
try localMediaProgress.updateFromPlaybackSession(session)
|
||||
|
||||
NSLog("Local progress saved to the database")
|
||||
logger.log("Local progress saved to the database")
|
||||
|
||||
// Send the local progress back to front-end
|
||||
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.localProgress.rawValue), object: nil)
|
||||
@@ -104,7 +106,7 @@ class PlayerProgress {
|
||||
|
||||
private func updateAllServerSessionFromLocalSession() async throws {
|
||||
try await withThrowingTaskGroup(of: Void.self) { [self] group in
|
||||
for session in try await Realm().objects(PlaybackSession.self).where({ $0.serverConnectionConfigId == Store.serverConfig?.id }) {
|
||||
for session in try Realm(queue: nil).objects(PlaybackSession.self).where({ $0.serverConnectionConfigId == Store.serverConfig?.id }) {
|
||||
let session = session.freeze()
|
||||
group.addTask {
|
||||
try await self.updateServerSessionFromLocalSession(session)
|
||||
@@ -141,7 +143,7 @@ class PlayerProgress {
|
||||
session = session.freeze()
|
||||
|
||||
guard safeToSync else { return }
|
||||
NSLog("Sending sessionId(\(session.id)) to server with currentTime(\(session.currentTime))")
|
||||
logger.log("Sending sessionId(\(session.id)) to server with currentTime(\(session.currentTime))")
|
||||
|
||||
var success = false
|
||||
if session.isLocal {
|
||||
@@ -161,25 +163,25 @@ class PlayerProgress {
|
||||
}
|
||||
|
||||
private func updateLocalSessionFromServerMediaProgress() async throws {
|
||||
NSLog("updateLocalSessionFromServerMediaProgress: Checking if local media progress was updated on server")
|
||||
guard let session = try await Realm().objects(PlaybackSession.self).last(where: {
|
||||
logger.log("updateLocalSessionFromServerMediaProgress: Checking if local media progress was updated on server")
|
||||
guard let session = try Realm(queue: nil).objects(PlaybackSession.self).last(where: {
|
||||
$0.isActiveSession == true && $0.serverConnectionConfigId == Store.serverConfig?.id
|
||||
})?.freeze() else {
|
||||
NSLog("updateLocalSessionFromServerMediaProgress: Failed to get session")
|
||||
logger.log("updateLocalSessionFromServerMediaProgress: Failed to get session")
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch the current progress
|
||||
let progress = await ApiClient.getMediaProgress(libraryItemId: session.libraryItemId!, episodeId: session.episodeId)
|
||||
guard let progress = progress else {
|
||||
NSLog("updateLocalSessionFromServerMediaProgress: No progress object")
|
||||
logger.log("updateLocalSessionFromServerMediaProgress: No progress object")
|
||||
return
|
||||
}
|
||||
|
||||
// Determine which session is newer
|
||||
let serverLastUpdate = progress.lastUpdate
|
||||
guard let localLastUpdate = session.updatedAt else {
|
||||
NSLog("updateLocalSessionFromServerMediaProgress: No local session updatedAt")
|
||||
logger.log("updateLocalSessionFromServerMediaProgress: No local session updatedAt")
|
||||
return
|
||||
}
|
||||
let serverCurrentTime = progress.currentTime
|
||||
@@ -190,16 +192,16 @@ class PlayerProgress {
|
||||
|
||||
// Update the session, if needed
|
||||
if serverIsNewerThanLocal && currentTimeIsDifferent {
|
||||
NSLog("updateLocalSessionFromServerMediaProgress: Server has newer time than local serverLastUpdate=\(serverLastUpdate) localLastUpdate=\(localLastUpdate)")
|
||||
logger.log("updateLocalSessionFromServerMediaProgress: Server has newer time than local serverLastUpdate=\(serverLastUpdate) localLastUpdate=\(localLastUpdate)")
|
||||
guard let session = session.thaw() else { return }
|
||||
try session.update {
|
||||
session.currentTime = serverCurrentTime
|
||||
session.updatedAt = serverLastUpdate
|
||||
}
|
||||
NSLog("updateLocalSessionFromServerMediaProgress: Updated session currentTime newCurrentTime=\(serverCurrentTime) previousCurrentTime=\(localCurrentTime)")
|
||||
logger.log("updateLocalSessionFromServerMediaProgress: Updated session currentTime newCurrentTime=\(serverCurrentTime) previousCurrentTime=\(localCurrentTime)")
|
||||
PlayerHandler.seek(amount: session.currentTime)
|
||||
} else {
|
||||
NSLog("updateLocalSessionFromServerMediaProgress: Local session does not need updating; local has latest progress")
|
||||
logger.log("updateLocalSessionFromServerMediaProgress: Local session does not need updating; local has latest progress")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
//
|
||||
// PlayerTimeUtils.swift
|
||||
// Audiobookshelf
|
||||
//
|
||||
// Created by Ron Heft on 9/20/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class PlayerTimeUtils {
|
||||
|
||||
private init() {}
|
||||
|
||||
static func calcSeekBackTime(currentTime: TimeInterval, lastPlayedMs: Double?) -> TimeInterval {
|
||||
let sinceLastPlayed = timeSinceLastPlayed(lastPlayedMs)
|
||||
let timeToSeekBack = timeToSeekBackForSinceLastPlayed(sinceLastPlayed)
|
||||
let currentTimeAfterSeekBack = currentTime.advanced(by: -timeToSeekBack)
|
||||
return max(currentTimeAfterSeekBack, 0)
|
||||
}
|
||||
|
||||
static internal func timeSinceLastPlayed(_ lastPlayedMs: Double?) -> TimeInterval? {
|
||||
guard let lastPlayedMs = lastPlayedMs else { return nil }
|
||||
let lastPlayed = Date(timeIntervalSince1970: lastPlayedMs / 1000)
|
||||
return lastPlayed.timeIntervalSinceNow
|
||||
}
|
||||
|
||||
static internal func timeToSeekBackForSinceLastPlayed(_ sinceLastPlayed: TimeInterval?) -> TimeInterval {
|
||||
if let sinceLastPlayed = sinceLastPlayed {
|
||||
if sinceLastPlayed < 6 {
|
||||
return 2
|
||||
} else if sinceLastPlayed < 12 {
|
||||
return 10
|
||||
} else if sinceLastPlayed < 30 {
|
||||
return 15
|
||||
} else if sinceLastPlayed < 180 {
|
||||
return 20
|
||||
} else if sinceLastPlayed < 3600 {
|
||||
return 25
|
||||
} else {
|
||||
return 29
|
||||
}
|
||||
} else {
|
||||
return 5
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -9,6 +9,8 @@ import Foundation
|
||||
import Alamofire
|
||||
|
||||
class ApiClient {
|
||||
private static let logger = AppLogger(category: "ApiClient")
|
||||
|
||||
public static func getData(from url: URL, completion: @escaping (UIImage?) -> Void) {
|
||||
URLSession.shared.dataTask(with: url, completionHandler: {(data, response, error) in
|
||||
if let data = data {
|
||||
@@ -19,7 +21,7 @@ class ApiClient {
|
||||
|
||||
public static func postResource<T: Decodable>(endpoint: String, parameters: [String: Any], decodable: T.Type = T.self, callback: ((_ param: T) -> Void)?) {
|
||||
if (Store.serverConfig == nil) {
|
||||
NSLog("Server config not set")
|
||||
logger.error("Server config not set")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -32,7 +34,7 @@ class ApiClient {
|
||||
case .success(let obj):
|
||||
callback?(obj)
|
||||
case .failure(let error):
|
||||
NSLog("api request to \(endpoint) failed")
|
||||
logger.error("api request to \(endpoint) failed")
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
@@ -40,7 +42,7 @@ class ApiClient {
|
||||
|
||||
public static func postResource<T: Encodable, U: Decodable>(endpoint: String, parameters: T, decodable: U.Type = U.self, callback: ((_ param: U) -> Void)?) {
|
||||
if (Store.serverConfig == nil) {
|
||||
NSLog("Server config not set")
|
||||
logger.error("Server config not set")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -53,7 +55,7 @@ class ApiClient {
|
||||
case .success(let obj):
|
||||
callback?(obj)
|
||||
case .failure(let error):
|
||||
NSLog("api request to \(endpoint) failed")
|
||||
logger.error("api request to \(endpoint) failed")
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
@@ -69,7 +71,7 @@ class ApiClient {
|
||||
|
||||
public static func postResource<T:Encodable>(endpoint: String, parameters: T, callback: ((_ success: Bool) -> Void)?) {
|
||||
if (Store.serverConfig == nil) {
|
||||
NSLog("Server config not set")
|
||||
logger.error("Server config not set")
|
||||
callback?(false)
|
||||
return
|
||||
}
|
||||
@@ -83,7 +85,7 @@ class ApiClient {
|
||||
case .success(_):
|
||||
callback?(true)
|
||||
case .failure(let error):
|
||||
NSLog("api request to \(endpoint) failed")
|
||||
logger.error("api request to \(endpoint) failed")
|
||||
print(error)
|
||||
|
||||
callback?(false)
|
||||
@@ -93,7 +95,7 @@ class ApiClient {
|
||||
|
||||
public static func patchResource<T: Encodable>(endpoint: String, parameters: T, callback: ((_ success: Bool) -> Void)?) {
|
||||
if (Store.serverConfig == nil) {
|
||||
NSLog("Server config not set")
|
||||
logger.error("Server config not set")
|
||||
callback?(false)
|
||||
return
|
||||
}
|
||||
@@ -107,7 +109,7 @@ class ApiClient {
|
||||
case .success(_):
|
||||
callback?(true)
|
||||
case .failure(let error):
|
||||
NSLog("api request to \(endpoint) failed")
|
||||
logger.error("api request to \(endpoint) failed")
|
||||
print(error)
|
||||
callback?(false)
|
||||
}
|
||||
@@ -124,7 +126,7 @@ class ApiClient {
|
||||
|
||||
public static func getResource<T: Decodable>(endpoint: String, decodable: T.Type = T.self, callback: ((_ param: T?) -> Void)?) {
|
||||
if (Store.serverConfig == nil) {
|
||||
NSLog("Server config not set")
|
||||
logger.error("Server config not set")
|
||||
callback?(nil)
|
||||
return
|
||||
}
|
||||
@@ -138,7 +140,7 @@ class ApiClient {
|
||||
case .success(let obj):
|
||||
callback?(obj)
|
||||
case .failure(let error):
|
||||
NSLog("api request to \(endpoint) failed")
|
||||
logger.error("api request to \(endpoint) failed")
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
@@ -193,10 +195,10 @@ class ApiClient {
|
||||
|
||||
if ( !localMediaProgressList.isEmpty ) {
|
||||
let payload = LocalMediaProgressSyncPayload(localMediaProgress: localMediaProgressList)
|
||||
NSLog("Sending sync local progress request with \(localMediaProgressList.count) progress items")
|
||||
logger.log("Sending sync local progress request with \(localMediaProgressList.count) progress items")
|
||||
postResource(endpoint: "api/me/sync-local-progress", parameters: payload, decodable: MediaProgressSyncResponsePayload.self) { response in
|
||||
let resultsPayload = LocalMediaProgressSyncResultsPayload(numLocalMediaProgressForServer: localMediaProgressList.count, numServerProgressUpdates: response.numServerProgressUpdates, numLocalProgressUpdates: response.localProgressUpdates?.count)
|
||||
NSLog("Media Progress Sync | \(String(describing: try? resultsPayload.asDictionary()))")
|
||||
logger.log("Media Progress Sync | \(String(describing: try? resultsPayload.asDictionary()))")
|
||||
|
||||
if let updates = response.localProgressUpdates {
|
||||
for update in updates {
|
||||
@@ -212,13 +214,13 @@ class ApiClient {
|
||||
callback(resultsPayload)
|
||||
}
|
||||
} else {
|
||||
NSLog("No local media progress to sync")
|
||||
logger.log("No local media progress to sync")
|
||||
callback(LocalMediaProgressSyncResultsPayload(numLocalMediaProgressForServer: 0, numServerProgressUpdates: 0, numLocalProgressUpdates: 0))
|
||||
}
|
||||
}
|
||||
|
||||
public static func updateMediaProgress<T:Encodable>(libraryItemId: String, episodeId: String?, payload: T, callback: @escaping () -> Void) {
|
||||
NSLog("updateMediaProgress \(libraryItemId) \(episodeId ?? "NIL") \(payload)")
|
||||
logger.log("updateMediaProgress \(libraryItemId) \(episodeId ?? "NIL") \(payload)")
|
||||
let endpoint = episodeId?.isEmpty ?? true ? "api/me/progress/\(libraryItemId)" : "api/me/progress/\(libraryItemId)/\(episodeId ?? "")"
|
||||
patchResource(endpoint: endpoint, parameters: payload) { success in
|
||||
callback()
|
||||
@@ -226,7 +228,7 @@ class ApiClient {
|
||||
}
|
||||
|
||||
public static func getMediaProgress(libraryItemId: String, episodeId: String?) async -> MediaProgress? {
|
||||
NSLog("getMediaProgress \(libraryItemId) \(episodeId ?? "NIL")")
|
||||
logger.log("getMediaProgress \(libraryItemId) \(episodeId ?? "NIL")")
|
||||
let endpoint = episodeId?.isEmpty ?? true ? "api/me/progress/\(libraryItemId)" : "api/me/progress/\(libraryItemId)/\(episodeId ?? "")"
|
||||
return await getResource(endpoint: endpoint, decodable: MediaProgress.self)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ class Database {
|
||||
public static var shared = {
|
||||
return Database()
|
||||
}()
|
||||
|
||||
private let logger = AppLogger(category: "Database")
|
||||
|
||||
private init() {}
|
||||
|
||||
@@ -30,7 +32,7 @@ class Database {
|
||||
existing.token = config.token
|
||||
}
|
||||
} catch {
|
||||
NSLog("failed to update server config")
|
||||
logger.error("failed to update server config")
|
||||
debugPrint(error)
|
||||
}
|
||||
|
||||
@@ -51,7 +53,7 @@ class Database {
|
||||
realm.add(config)
|
||||
}
|
||||
} catch(let exception) {
|
||||
NSLog("failed to save server config")
|
||||
logger.error("failed to save server config")
|
||||
debugPrint(exception)
|
||||
}
|
||||
|
||||
@@ -70,7 +72,7 @@ class Database {
|
||||
}
|
||||
}
|
||||
} catch(let exception) {
|
||||
NSLog("failed to delete server config")
|
||||
logger.error("failed to delete server config")
|
||||
debugPrint(exception)
|
||||
}
|
||||
}
|
||||
@@ -101,7 +103,7 @@ class Database {
|
||||
}
|
||||
}
|
||||
} catch(let exception) {
|
||||
NSLog("failed to save server config active index")
|
||||
logger.error("failed to save server config active index")
|
||||
debugPrint(exception)
|
||||
}
|
||||
}
|
||||
@@ -121,7 +123,7 @@ class Database {
|
||||
realm.add(deviceSettings)
|
||||
}
|
||||
} catch {
|
||||
NSLog("failed to save device settings")
|
||||
logger.error("failed to save device settings")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,6 +244,7 @@ class Database {
|
||||
public func getPlaybackSession(id: String) -> PlaybackSession? {
|
||||
do {
|
||||
let realm = try Realm()
|
||||
realm.refresh() // Refresh, because working with stale sessions leads to wrong times
|
||||
return realm.object(ofType: PlaybackSession.self, forPrimaryKey: id)
|
||||
} catch {
|
||||
debugPrint(error)
|
||||
|
||||
@@ -59,7 +59,7 @@ extension CAPPluginCall {
|
||||
let json = try JSONSerialization.data(withJSONObject: value)
|
||||
return try JSONDecoder().decode(type, from: json)
|
||||
} catch {
|
||||
NSLog("Failed to get json for \(key)")
|
||||
AppLogger().error("Failed to get json for \(key)")
|
||||
debugPrint(error)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
//
|
||||
// AppLogger.swift
|
||||
// AppLogger
|
||||
//
|
||||
// Created by Fernando Fernandes on 29.06.20.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import os
|
||||
|
||||
/// Logs app info by using the newest Swift unified logging APIs.
|
||||
///
|
||||
/// Reference:
|
||||
/// - [Explore logging in Swift (WWDC20)](https://developer.apple.com/wwdc20/10168)
|
||||
/// - [Unified Logging](https://developer.apple.com/documentation/os/logging)
|
||||
/// - [OSLog](https://developer.apple.com/documentation/os/oslog)
|
||||
/// - [Logger](https://developer.apple.com/documentation/os/logger)
|
||||
public struct AppLogger {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
/// Default values used by the `AppLogger`.
|
||||
public struct Defaults {
|
||||
public static let subsystem = Bundle.main.bundleIdentifier ?? "ABS"
|
||||
public static let category = "default"
|
||||
public static let isPrivate = false
|
||||
}
|
||||
|
||||
// MARK: - Private Properties
|
||||
|
||||
private let logger: Logger
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
/// Creates an `AppLogger` instance.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - subsystem: `String`. Organizes large topic areas within the app or apps. For example, you might define
|
||||
/// a subsystem for each process that you create. The default is `Bundle.main.bundleIdentifier ?? "AppLogger"`.
|
||||
/// - category: `String`. Within a `subsystem`, you define categories to further distinguish parts of that
|
||||
/// subsystem. For example, if you used a single subsystem for your app, you might create separate categories for
|
||||
/// model code and user-interface code. In a game, you might use categories to distinguish between physics, AI,
|
||||
/// world simulation, and rendering. The default is `default`.
|
||||
public init(subsystem: String = Defaults.subsystem, category: String = Defaults.category) {
|
||||
self.logger = Logger(subsystem: subsystem, category: category)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Interface
|
||||
|
||||
public extension AppLogger {
|
||||
|
||||
func log(_ information: String, isPrivate: Bool = Defaults.isPrivate) {
|
||||
if isPrivate {
|
||||
logger.log("\(information, privacy: .private)")
|
||||
} else {
|
||||
logger.log("\(information, privacy: .public)")
|
||||
}
|
||||
}
|
||||
|
||||
func error(_ information: String, isPrivate: Bool = Defaults.isPrivate) {
|
||||
if isPrivate {
|
||||
logger.error("\(information, privacy: .private)")
|
||||
} else {
|
||||
logger.error("\(information, privacy: .public)")
|
||||
}
|
||||
}
|
||||
|
||||
func error(_ error: Error) {
|
||||
logger.error("\(String(describing: error))")
|
||||
}
|
||||
}
|
||||
@@ -11,15 +11,22 @@ import MediaPlayer
|
||||
struct NowPlayingMetadata {
|
||||
var id: String
|
||||
var itemId: String
|
||||
var artworkUrl: String?
|
||||
var title: String
|
||||
var author: String?
|
||||
var series: String?
|
||||
|
||||
var coverUrl: URL? {
|
||||
guard let config = Store.serverConfig else { return nil }
|
||||
guard let url = URL(string: "\(config.address)/api/items/\(itemId)/cover?token=\(config.token)") else { return nil }
|
||||
return url
|
||||
if self.isLocal {
|
||||
guard let item = Database.shared.getLocalLibraryItem(byServerLibraryItemId: self.itemId) else { return nil }
|
||||
return item.coverUrl
|
||||
} else {
|
||||
guard let config = Store.serverConfig else { return nil }
|
||||
guard let url = URL(string: "\(config.address)/api/items/\(itemId)/cover?token=\(config.token)") else { return nil }
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
var isLocal: Bool { id.starts(with: "play_local_") }
|
||||
}
|
||||
|
||||
class NowPlayingInfo {
|
||||
@@ -34,28 +41,17 @@ class NowPlayingInfo {
|
||||
|
||||
public func setSessionMetadata(metadata: NowPlayingMetadata) {
|
||||
setMetadata(artwork: nil, metadata: metadata)
|
||||
|
||||
let isLocalItem = metadata.itemId.starts(with: "local_")
|
||||
if isLocalItem {
|
||||
guard let artworkUrl = metadata.artworkUrl else { return }
|
||||
let coverImage = UIImage(contentsOfFile: artworkUrl)
|
||||
guard let coverImage = coverImage else { return }
|
||||
let artwork = MPMediaItemArtwork(boundsSize: coverImage.size) { _ -> UIImage in
|
||||
return coverImage
|
||||
guard let url = metadata.coverUrl else { return }
|
||||
// For local images, "downloading" is occurring off disk, hence this code path works as expected
|
||||
ApiClient.getData(from: url) { [self] image in
|
||||
guard let downloadedImage = image else {
|
||||
return
|
||||
}
|
||||
let artwork = MPMediaItemArtwork.init(boundsSize: downloadedImage.size, requestHandler: { _ -> UIImage in
|
||||
return downloadedImage
|
||||
})
|
||||
|
||||
self.setMetadata(artwork: artwork, metadata: metadata)
|
||||
} else {
|
||||
guard let url = metadata.coverUrl else { return }
|
||||
ApiClient.getData(from: url) { [self] image in
|
||||
guard let downloadedImage = image else {
|
||||
return
|
||||
}
|
||||
let artwork = MPMediaItemArtwork.init(boundsSize: downloadedImage.size, requestHandler: { _ -> UIImage in
|
||||
return downloadedImage
|
||||
})
|
||||
|
||||
self.setMetadata(artwork: artwork, metadata: metadata)
|
||||
}
|
||||
}
|
||||
}
|
||||
public func update(duration: Double, currentTime: Double, rate: Float) {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "audiobookshelf-app",
|
||||
"version": "0.9.58-beta",
|
||||
"version": "0.9.59-beta",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "audiobookshelf-app",
|
||||
"version": "0.9.57-beta",
|
||||
"version": "0.9.59-beta",
|
||||
"dependencies": {
|
||||
"@capacitor/android": "^3.4.3",
|
||||
"@capacitor/app": "^1.1.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf-app",
|
||||
"version": "0.9.58-beta",
|
||||
"version": "0.9.59-beta",
|
||||
"author": "advplyr",
|
||||
"scripts": {
|
||||
"dev": "nuxt --hostname 0.0.0.0 --port 1337",
|
||||
|
||||
@@ -58,7 +58,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
bookCoverAspectRatio() {
|
||||
return this.$store.getters['getBookCoverAspectRatio']
|
||||
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||
},
|
||||
bookItems() {
|
||||
return this.collection.books || []
|
||||
|
||||
@@ -199,7 +199,7 @@ export default {
|
||||
return null
|
||||
},
|
||||
bookCoverAspectRatio() {
|
||||
return this.$store.getters['getBookCoverAspectRatio']
|
||||
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||
},
|
||||
libraryItemId() {
|
||||
return this.libraryItem.id
|
||||
|
||||
@@ -68,7 +68,7 @@ export default {
|
||||
return this.$store.state.libraries.currentLibraryId
|
||||
},
|
||||
bookCoverAspectRatio() {
|
||||
return this.$store.getters['getBookCoverAspectRatio']
|
||||
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||
},
|
||||
totalResults() {
|
||||
return this.bookResults.length + this.seriesResults.length + this.authorResults.length + this.podcastResults.length
|
||||
|
||||
@@ -45,7 +45,8 @@ const Constants = {
|
||||
}
|
||||
|
||||
export {
|
||||
PlayerState
|
||||
PlayerState,
|
||||
BookCoverAspectRatio
|
||||
}
|
||||
|
||||
export default ({ app }, inject) => {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<metadata>Generated by IcoMoon</metadata>
|
||||
<defs>
|
||||
<font id="absicons" horiz-adv-x="1024">
|
||||
<font-face units-per-em="1024" ascent="960" descent="-64" />
|
||||
<missing-glyph horiz-adv-x="1024" />
|
||||
<glyph unicode=" " horiz-adv-x="512" d="" />
|
||||
<glyph unicode="" glyph-name="audiobookshelf" horiz-adv-x="862" d="M854.995 455.649c-5.287 4.402-13.511 10.896-24.656 18.576v86.499c0 220.513-178.763 399.276-399.281 399.276v0c-220.513 0-399.276-178.763-399.276-399.276v-86.499c-11.145-7.697-19.369-14.174-24.656-18.576-4.368-3.659-7.126-9.116-7.126-15.217 0-0.006 0-0.011 0-0.017v0.001-103.619c0-0.011 0-0.025 0-0.038 0-6.101 2.758-11.558 7.095-15.191l0.031-0.025c12.326-10.26 40.612-31.782 84.651-53.849v-10.072c0-27.162 17.525-49.186 39.139-49.186v0c21.614 0 39.139 22.023 39.139 49.186v248.534c0 27.166-17.525 49.19-39.139 49.19v0c-20.707 0-37.657-20.215-39.043-45.795l-0.096-0.048v51.221c0 187.38 151.902 339.278 339.282 339.278v0c187.38 0 339.282-151.898 339.282-339.278v-51.221l-0.096 0.048c-1.386 25.58-18.332 45.795-39.043 45.795v0c-21.614 0-39.135-22.023-39.135-49.19v-248.512c0-27.162 17.521-49.186 39.135-49.186v0c21.618 0 39.139 22.023 39.139 49.186v10.072c44.042 22.067 72.329 43.585 84.655 53.849 4.368 3.661 7.126 9.12 7.126 15.223 0 0.011 0 0.022 0 0.034v-0.002 103.597c0 0.003 0 0.007 0 0.010 0 6.103-2.758 11.561-7.095 15.198l-0.031 0.025zM291.835 23.619c26.11 0 47.276 21.166 47.276 47.276v0 529.382c0 0.001 0 0.003 0 0.004 0 26.11-21.166 47.276-47.276 47.276h-48.846c-26.112 0-47.281-21.168-47.281-47.281v0-529.382c0-26.11 21.166-47.276 47.276-47.276 0.002 0 0.003 0 0.005 0v0zM213.535 520.595h107.764v-11.258h-107.764zM455.484 23.619c26.11 0 47.276 21.166 47.276 47.276v0 529.382c0 0.001 0 0.003 0 0.004 0 26.11-21.166 47.276-47.276 47.276h-48.846c-26.112 0-47.281-21.168-47.281-47.281v0-529.382c0-26.11 21.166-47.276 47.276-47.276 0.002 0 0.003 0 0.005 0v0zM377.183 520.595h107.75v-11.258h-107.75zM619.132 23.619c26.11 0 47.276 21.166 47.276 47.276v0 529.382c0 0.001 0 0.003 0 0.004 0 26.11-21.166 47.276-47.276 47.276h-48.846c-26.112 0-47.281-21.168-47.281-47.281v0-529.382c0-26.11 21.166-47.276 47.276-47.276 0.002 0 0.003 0 0.005 0v0zM540.831 520.595h107.75v-11.258h-107.75zM114.101 7.257h633.92c19.676 0 35.626-15.95 35.626-35.626v-0.004c0-19.676-15.95-35.626-35.626-35.626h-633.92c-19.676 0-35.626 15.95-35.626 35.626v0.004c0 19.676 15.95 35.626 35.626 35.626z" />
|
||||
<glyph unicode="" glyph-name="microphone-2" horiz-adv-x="713" d="M713.333 484.083c0 26.75-21.667 48.333-48.333 48.333-26.75 0-48.333-21.667-48.333-48.333 0-95.25-22.833-173.917-69.083-226.417-41.917-47.5-105.25-74.917-190.833-74.917s-148.917 27.333-190.833 74.833c-46.333 52.5-69.083 131.167-69.083 226.5 0 26.75-21.667 48.333-48.333 48.333-26.75 0-48.333-21.667-48.333-48.333 0-118.75 30.75-219.417 93.25-290.333 46-52.167 108-87.5 186.083-101.25v-79.333h-118.5c-21.167 0-38.5-17.333-38.5-38.5v-38.583h468.667v38.583c0 21.167-17.333 38.5-38.5 38.5h-118.75v79.333c78.083 13.833 140 49.083 186.083 101.333 62.5 70.833 93.333 171.5 93.333 290.25v0 0zM356.667 960c98.667 0 179.333-80.75 179.333-179.333v-4.667h-96.417v-114.25h96.5v-58.167h-96.5v-114.25h96.5v-41.083c0-98.667-80.75-179.333-179.333-179.333-98.667 0-179.333 80.75-179.333 179.333v41.083h96.5v114.25h-96.583v58.167h96.5v114.25h-96.5v4.667c0 98.583 80.667 179.333 179.333 179.333v0z" />
|
||||
<glyph unicode="" glyph-name="microphone-1" horiz-adv-x="768" d="M192 768v-320c0-106 86-192 192-192s192 86 192 192h-160c-17.6 0-32 14.4-32 32s14.4 32 32 32h160v64h-160c-17.6 0-32 14.4-32 32s14.4 32 32 32h160v64h-160c-17.6 0-32 14.4-32 32s14.4 32 32 32h160c0 106-86 192-192 192s-192-86-192-192zM640 480v-32c0-141.4-114.6-256-256-256s-256 114.6-256 256v80c0 26.6-21.4 48-48 48s-48-21.4-48-48v-80c0-178.2 132.4-325.4 304-348.8v-67.2h-96c-26.6 0-48-21.4-48-48s21.4-48 48-48h288c26.6 0 48 21.4 48 48s-21.4 48-48 48h-96v67.2c171.6 23.4 304 170.6 304 348.8v80c0 26.6-21.4 48-48 48s-48-21.4-48-48v-48z" />
|
||||
<glyph unicode="" glyph-name="radio" d="M989.6 866c25.4 7.4 40 34.2 32.6 59.6s-34.2 40-59.8 32.4l-859-251.8c-18.8-5.4-35.8-14.6-50.2-26.4-32.2-23.2-53.2-61-53.2-103.8v-512c0-70.6 57.4-128 128-128h768c70.6 0 128 57.4 128 128v512c0 70.6-57.4 128-128 128h-459l552.6 162zM736 160c-88.4 0-160 71.6-160 160s71.6 160 160 160 160-71.6 160-160-71.6-160-160-160zM160 448c0 17.6 14.4 32 32 32h192c17.6 0 32-14.4 32-32s-14.4-32-32-32h-192c-17.6 0-32 14.4-32 32zM128 320c0 17.6 14.4 32 32 32h256c17.6 0 32-14.4 32-32s-14.4-32-32-32h-256c-17.6 0-32 14.4-32 32zM160 192c0 17.6 14.4 32 32 32h192c17.6 0 32-14.4 32-32s-14.4-32-32-32h-192c-17.6 0-32 14.4-32 32z" />
|
||||
<glyph unicode="" glyph-name="podcast" horiz-adv-x="1043" d="M585.253 366.417v-430.408h-127.13v430.408c-41.76 22.603-70.128 66.8-70.128 117.637 0 73.822 59.863 133.685 133.693 133.685 73.822 0 133.693-59.863 133.693-133.685 0-50.837-28.369-95.033-70.128-117.637v0zM333.872 865.196c24.76 12.21 34.907 42.193 22.697 66.927-12.21 24.743-42.167 34.907-66.91 22.705-101.045-49.843-179.63-132.301-229.287-229.397-36.682-71.708-57.596-151.499-60.117-232.021-2.539-81.201 13.475-163.226 50.718-238.72 45.861-93.004 123.614-175.725 238.084-234.288 24.531-12.541 54.59-2.802 67.123 21.729 12.541 24.531 2.811 54.581-21.72 67.114-93.717 47.933-156.968 114.843-193.879 189.642-29.77 60.381-42.575 126.153-40.537 191.382 2.063 65.866 19.156 131.12 49.164 189.76 40.18 78.552 103.49 145.139 184.666 185.167v0zM753.718 954.837c-24.743 12.21-54.709 2.038-66.919-22.705s-2.055-54.726 22.705-66.927c81.184-40.036 144.494-106.615 184.657-185.175 29.999-58.631 47.1-123.886 49.155-189.76 2.038-65.229-10.767-131.002-40.528-191.382-36.911-74.799-100.162-141.709-193.879-189.642-24.531-12.541-34.253-42.592-21.72-67.114 12.533-24.531 42.592-34.262 67.123-21.729 114.461 58.555 192.223 141.284 238.084 234.288 37.242 75.495 53.257 157.52 50.718 238.72-2.522 80.522-23.436 160.313-60.109 232.021-49.665 97.096-128.242 179.554-229.287 229.406v0zM702.992 734.942c-18.638 13.425-44.63 9.187-58.054-9.451-13.416-18.638-9.187-44.63 9.451-58.054 7.515-5.409 14.885-11.48 22.111-18.12 42.21-38.915 67.241-90.804 72.498-145.708 5.315-55.396-9.434-114.096-46.854-166.019-6.351-8.822-13.671-17.764-21.95-26.747-15.581-16.914-14.503-43.288 2.437-58.861 16.931-15.573 43.296-14.477 58.869 2.437 10.020 10.86 19.411 22.425 28.148 34.576 49.775 69.11 69.339 147.746 62.172 222.443-7.201 75.172-41.36 146.116-98.905 199.177-9.052 8.347-19.029 16.464-29.923 24.327v0zM388.98 667.412c18.638 13.425 22.875 39.416 9.451 58.054s-39.416 22.875-58.054 9.459c-10.903-7.863-20.871-15.98-29.923-24.319-57.553-53.070-91.713-124.005-98.914-199.177-7.167-74.688 12.397-153.35 62.181-222.46 8.754-12.142 18.137-23.699 28.148-34.559 15.573-16.914 41.938-18.010 58.861-2.437 16.948 15.573 18.027 41.938 2.445 58.861-8.279 8.992-15.598 17.925-21.95 26.747-37.412 51.923-52.17 110.614-46.854 166.011 5.256 54.895 30.288 106.793 72.498 145.708 7.234 6.632 14.605 12.703 22.111 18.112v0z" />
|
||||
<glyph unicode="" glyph-name="books-1" d="M384 832v-640h128v640h-128zM512 746.667l170.667-554.667 128 42.667-170.667 554.667-128-42.667zM213.333 746.667v-554.667h128v554.667h-128zM128 149.333v-85.333h768v85.333h-768z" />
|
||||
<glyph unicode="" glyph-name="database-2" horiz-adv-x="876" d="M437.75 960c240.583 0 435.583-91.333 435.583-203.833 0-112.583-195.083-203.833-435.583-203.833s-435.583 91.333-435.583 203.833c0 112.5 195.083 203.833 435.583 203.833v0zM2.167 278.083v-156.5c77.5-275.25 843.167-222.083 871.25 14.083v156.333c-38.25-259.25-810-277.917-871.25-13.917v0 0zM0 685.5v-152.833c77.5-268.833 847.417-232.5 875.583-1.917v152.75c-38.333-253.25-814.333-255.833-875.583 2v0zM0 488v-156.5c77.5-275.25 847.417-238.083 875.583-1.917v156.333c-38.333-259.25-814.333-261.917-875.583 2.083v0z" />
|
||||
<glyph unicode="" glyph-name="headphones" d="M288 384h-64v-448h64c17.6 0 32 14.4 32 32v384c0 17.6-14.4 32-32 32zM736 384c-17.602 0-32-14.4-32-32v-384c0-17.6 14.398-32 32-32h64v448h-64zM1024 448c0 282.77-229.23 512-512 512s-512-229.23-512-512c0-61.412 10.83-120.29 30.656-174.848-19.478-33.206-30.656-71.87-30.656-113.152 0-112.846 83.448-206.188 192-221.716v443.418c-31.914-4.566-61.664-15.842-87.754-32.378-5.392 26.718-8.246 54.364-8.246 82.676 0 229.75 186.25 416 416 416s416-186.25 416-416c0-28.314-2.83-55.968-8.22-82.696-26.1 16.546-55.854 27.848-87.78 32.418v-443.44c108.548 15.532 192 108.874 192 221.714 0 41.274-11.178 79.934-30.648 113.138 19.828 54.566 30.648 113.452 30.648 174.866z" />
|
||||
<glyph unicode="" glyph-name="music" d="M960 960h64v-736c0-88.366-100.29-160-224-160s-224 71.634-224 160c0 88.368 100.29 160 224 160 62.684 0 119.342-18.4 160-48.040v368.040l-512-113.778v-494.222c0-88.366-100.288-160-224-160s-224 71.634-224 160c0 88.368 100.288 160 224 160 62.684 0 119.342-18.4 160-48.040v624.040l576 128z" />
|
||||
<glyph unicode="" glyph-name="video" d="M384 672c0 88.366 71.634 160 160 160s160-71.634 160-160c0-88.366-71.634-160-160-160s-160 71.634-160 160zM0 672c0 88.366 71.634 160 160 160s160-71.634 160-160c0-88.366-71.634-160-160-160s-160 71.634-160 160zM768 352v96c0 35.2-28.8 64-64 64h-640c-35.2 0-64-28.8-64-64v-320c0-35.2 28.8-64 64-64h640c35.2 0 64 28.8 64 64v96l256-160v448l-256-160zM640 192h-512v192h512v-192z" />
|
||||
<glyph unicode="" glyph-name="microphone-3" d="M480 256c88.366 0 160 71.634 160 160v384c0 88.366-71.634 160-160 160s-160-71.634-160-160v-384c0-88.366 71.636-160 160-160zM704 512v-96c0-123.71-100.29-224-224-224-123.712 0-224 100.29-224 224v96h-64v-96c0-148.238 112.004-270.3 256-286.22v-129.78h-128v-64h320v64h-128v129.78c143.994 15.92 256 137.982 256 286.22v96h-64z" />
|
||||
<glyph unicode="" glyph-name="book" d="M896 832v-832h-672c-53.026 0-96 42.98-96 96s42.974 96 96 96h608v768h-640c-70.398 0-128-57.6-128-128v-768c0-70.4 57.602-128 128-128h768v896h-64zM224.056 128v0c-0.018-0.002-0.038 0-0.056 0-17.672 0-32-14.326-32-32s14.328-32 32-32c0.018 0 0.038 0.002 0.056 0.002v-0.002h607.89v64h-607.89z" />
|
||||
<glyph unicode="" glyph-name="books-2" horiz-adv-x="1152" d="M224 832h-192c-17.6 0-32-14.4-32-32v-704c0-17.6 14.4-32 32-32h192c17.6 0 32 14.4 32 32v704c0 17.6-14.4 32-32 32zM192 640h-128v64h128v-64zM544 832h-192c-17.6 0-32-14.4-32-32v-704c0-17.6 14.4-32 32-32h192c17.6 0 32 14.4 32 32v704c0 17.6-14.4 32-32 32zM512 640h-128v64h128v-64zM765.088 782.52l-171.464-86.394c-15.716-7.918-22.096-27.258-14.178-42.976l287.978-571.548c7.918-15.718 27.258-22.098 42.976-14.178l171.464 86.392c15.716 7.92 22.096 27.26 14.178 42.974l-287.978 571.55c-7.92 15.718-27.26 22.1-42.976 14.18z" />
|
||||
<glyph unicode="" glyph-name="file-picture" d="M832 64h-640v128l192 320 263-320 185 128v-256zM832 480c0-53.020-42.98-96-96-96-53.022 0-96 42.98-96 96s42.978 96 96 96c53.020 0 96-42.98 96-96zM917.806 730.924c-22.212 30.292-53.174 65.7-87.178 99.704s-69.412 64.964-99.704 87.178c-51.574 37.82-76.592 42.194-90.924 42.194h-496c-44.112 0-80-35.888-80-80v-864c0-44.112 35.888-80 80-80h736c44.112 0 80 35.888 80 80v624c0 14.332-4.372 39.35-42.194 90.924zM785.374 785.374c30.7-30.7 54.8-58.398 72.58-81.374h-153.954v153.946c22.984-17.78 50.678-41.878 81.374-72.572zM896 16c0-8.672-7.328-16-16-16h-736c-8.672 0-16 7.328-16 16v864c0 8.672 7.328 16 16 16 0 0 495.956 0.002 496 0v-224c0-17.672 14.326-32 32-32h224v-624z" />
|
||||
<glyph unicode="" glyph-name="database-1" d="M512 960c-282.77 0-512-71.634-512-160v-128c0-88.366 229.23-160 512-160s512 71.634 512 160v128c0 88.366-229.23 160-512 160zM512 416c-282.77 0-512 71.634-512 160v-192c0-88.366 229.23-160 512-160s512 71.634 512 160v192c0-88.366-229.23-160-512-160zM512 128c-282.77 0-512 71.634-512 160v-192c0-88.366 229.23-160 512-160s512 71.634 512 160v192c0-88.366-229.23-160-512-160z" />
|
||||
<glyph unicode="" glyph-name="rocket" d="M704 896l-320-320h-192l-192-256c0 0 203.416 56.652 322.066 30.084l-322.066-414.084 421.902 328.144c58.838-134.654-37.902-328.144-37.902-328.144l256 192v192l320 320 64 320-320-64z" />
|
||||
<glyph unicode="" glyph-name="power" d="M384 960l-384-512h384l-256-512 896 640h-512l384 384z" />
|
||||
<glyph unicode="" glyph-name="star" d="M1024 562.95l-353.78 51.408-158.22 320.582-158.216-320.582-353.784-51.408 256-249.538-60.432-352.352 316.432 166.358 316.432-166.358-60.434 352.352 256.002 249.538z" />
|
||||
<glyph unicode="" glyph-name="heart" d="M755.188 896c-107.63 0-200.258-87.554-243.164-179-42.938 91.444-135.578 179-243.216 179-148.382 0-268.808-120.44-268.808-268.832 0-301.846 304.5-380.994 512.022-679.418 196.154 296.576 511.978 387.206 511.978 679.418 0 148.392-120.43 268.832-268.812 268.832z" />
|
||||
<glyph unicode="" glyph-name="rss" d="M136.294 209.070c-75.196 0-136.292-61.334-136.292-136.076 0-75.154 61.1-135.802 136.292-135.802 75.466 0 136.494 60.648 136.494 135.802-0.002 74.742-61.024 136.076-136.494 136.076zM0.156 612.070v-196.258c127.784 0 247.958-49.972 338.458-140.512 90.384-90.318 140.282-211.036 140.282-339.3h197.122c-0.002 372.82-303.282 676.070-675.862 676.070zM0.388 960v-196.356c455.782 0 826.756-371.334 826.756-827.644h196.856c0 564.47-459.254 1024-1023.612 1024z" />
|
||||
</font></defs></svg>
|
||||
|
After Width: | Height: | Size: 13 KiB |
@@ -32,7 +32,8 @@ export const state = () => ({
|
||||
icon: 'replay_30',
|
||||
value: 30
|
||||
}
|
||||
]
|
||||
],
|
||||
libraryIcons: ['database', 'audiobookshelf', 'books-1', 'books-2', 'book-1', 'microphone-1', 'microphone-3', 'radio', 'podcast', 'rss', 'headphones', 'music', 'file-picture', 'rocket', 'power', 'star', 'heart']
|
||||
})
|
||||
|
||||
export const getters = {
|
||||
|
||||
@@ -36,10 +36,6 @@ export const getters = {
|
||||
if (!state.serverSettings) return null
|
||||
return state.serverSettings[key]
|
||||
},
|
||||
getBookCoverAspectRatio: state => {
|
||||
if (!state.serverSettings) return 1
|
||||
return state.serverSettings.coverAspectRatio === 0 ? 1.6 : 1
|
||||
},
|
||||
getJumpForwardTime: state => {
|
||||
if (!state.deviceData || !state.deviceData.deviceSettings) return 10
|
||||
return state.deviceData.deviceSettings.jumpForwardTime || 10
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
const { BookCoverAspectRatio } = require('../plugins/constants')
|
||||
|
||||
export const state = () => ({
|
||||
libraries: [],
|
||||
lastLoad: 0,
|
||||
@@ -18,6 +20,14 @@ export const getters = {
|
||||
getCurrentLibraryMediaType: (state, getters) => {
|
||||
var currLib = getters.getCurrentLibrary
|
||||
return currLib ? currLib.mediaType : null
|
||||
},
|
||||
getCurrentLibrarySettings: (state, getters) => {
|
||||
if (!getters.getCurrentLibrary) return null
|
||||
return getters.getCurrentLibrary.settings
|
||||
},
|
||||
getBookCoverAspectRatio: (state, getters) => {
|
||||
if (!getters.getCurrentLibrarySettings || isNaN(getters.getCurrentLibrarySettings.coverAspectRatio)) return 1
|
||||
return getters.getCurrentLibrarySettings.coverAspectRatio === BookCoverAspectRatio.STANDARD ? 1.6 : 1
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@ module.exports = {
|
||||
purge: {
|
||||
options: {
|
||||
safelist: [
|
||||
'bg-success'
|
||||
'bg-success',
|
||||
'bg-info'
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||