diff --git a/.github/testing-page-template.html b/.github/testing-page-template.html new file mode 100644 index 00000000..8a3d33f2 --- /dev/null +++ b/.github/testing-page-template.html @@ -0,0 +1,81 @@ + + + + + +Audiobookshelf :: Testers + + + + + + + +

Audiobookshelf :: Testers

+ +

+ Here you can download the latest version of Audiobookshelf for Android for testing. + The app is automatically built for every new patch added to the project. +

+ +
+
Built on
+
__DATE__
+ +
Git commit
+ __COMMIT__ + +
Download
+ __APK__ +
+ +

+ Using this requires some technical knowledge to install the APK manually. + Regular users should use the releases published on the Play Store. +

+ +
+ +

Report bugs, request features, provide feedback, and contribute on GitHub

+

+ + + + + + + + + diff --git a/.github/workflows/build-apk.yml b/.github/workflows/build-apk.yml new file mode 100644 index 00000000..17517c89 --- /dev/null +++ b/.github/workflows/build-apk.yml @@ -0,0 +1,47 @@ +name: Build APK + +on: + push: + paths-ignore: + - 'ios/**' + - 'readme.md' + pull_request: + paths-ignore: + - 'ios/**' + - 'readme.md' +jobs: + main: + runs-on: ubuntu-latest + steps: + - name: checkout sources + uses: actions/checkout@v3 + + - name: use Node.js 16.x + uses: actions/setup-node@v3 + with: + node-version: 16 + + - name: install dependencies + run: npm ci + + - name: build Nuxt project + run: npm run generate + + - name: copy to Android project + run: npx cap sync + + - name: build Android app + run: ./android/gradlew assembleDebug -p android --no-daemon + + - name: rename apk + working-directory: android/app/build/outputs/apk/debug/ + run: | + build="$(date +%Y%m%d-%H%M%S)-$(git rev-parse --short HEAD)" + name="audiobookshelf-${build}.apk" + mv -v app-debug.apk "${name}" + + - name: upload app + uses: actions/upload-artifact@v3 + with: + name: audiobookshelf-apk + path: android/app/build/outputs/apk/debug/*.apk diff --git a/.github/workflows/deploy-apk.yml b/.github/workflows/deploy-apk.yml new file mode 100644 index 00000000..35c1e2c9 --- /dev/null +++ b/.github/workflows/deploy-apk.yml @@ -0,0 +1,76 @@ +name: Publish Test App + +on: + push: + branches: + - master + paths-ignore: + - 'ios/**' + - 'readme.md' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: checkout sources + uses: actions/checkout@v3 + + - name: use Node.js 16.x + uses: actions/setup-node@v3 + with: + node-version: 16 + + - name: install dependencies + run: npm ci + + - name: build Nuxt project + run: npm run generate + + - name: copy to Android project + run: npx cap sync + + - name: build Android app + run: ./android/gradlew assembleDebug -p android --no-daemon + + - name: rename apk + working-directory: android/app/build/outputs/apk/debug/ + run: | + build="$(date +%Y%m%d-%H%M%S)-$(git rev-parse --short HEAD)" + name="audiobookshelf-${build}.apk" + mv -v app-debug.apk "${name}" + + - name: prepare test page ressources + run: | + mkdir ghpages + cp android/app/build/outputs/apk/debug/*apk ghpages/ + cp static/Logo.png ghpages/logo.png + cp .github/testing-page-template.html ghpages/index.html + + - name: build test page + working-directory: ghpages + run: | + sed -i "s/__DATE__/$(date)/g" index.html + sed -i "s/__COMMIT__/$(git rev-parse --short HEAD)/g" index.html + sed -i "s/__APK__/$(ls *apk)/g" index.html + + - name: upload test page artifact + uses: actions/upload-pages-artifact@v1 + with: + path: ./ghpages + + deploy: + needs: build + + permissions: + pages: write + id-token: write + + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + runs-on: ubuntu-latest + steps: + - name: deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v1 diff --git a/android/app/src/main/java/com/audiobookshelf/app/device/FolderScanner.kt b/android/app/src/main/java/com/audiobookshelf/app/device/FolderScanner.kt index 7cdb49a0..fc6998fa 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/device/FolderScanner.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/device/FolderScanner.kt @@ -51,7 +51,7 @@ class FolderScanner(var ctx: Context) { var mediaItemsUpToDate = 0 // Search for files in media item folder - val foldersFound = df.search(false, DocumentFileType.FOLDER) + val foldersFound = df.search(true, DocumentFileType.FOLDER) // Match folders found with local library items already saved in db var existingLocalLibraryItems = DeviceManager.dbManager.getLocalLibraryItemsInFolder(localFolder.id) @@ -72,11 +72,16 @@ class FolderScanner(var ctx: Context) { Log.d(tag, "Iterating over Folder Found ${itemFolder.name} | ${itemFolder.getSimplePath(ctx)} | URI: ${itemFolder.uri}") val existingItem = existingLocalLibraryItems.find { emi -> emi.id == getLocalLibraryItemId(itemFolder.id) } - when (scanLibraryItemFolder(itemFolder, localFolder, existingItem, forceAudioProbe)) { - ItemScanResult.REMOVED -> mediaItemsRemoved++ - ItemScanResult.UPDATED -> mediaItemsUpdated++ - ItemScanResult.ADDED -> mediaItemsAdded++ - else -> mediaItemsUpToDate++ + val filesInFolder = itemFolder.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4", "application/octet-stream")) + + // Do not scan folders that have no media items and not an existing item already + if (existingItem != null || filesInFolder.isNotEmpty()) { + when (scanLibraryItemFolder(itemFolder, filesInFolder, localFolder, existingItem, forceAudioProbe)) { + ItemScanResult.REMOVED -> mediaItemsRemoved++ + ItemScanResult.UPDATED -> mediaItemsUpdated++ + ItemScanResult.ADDED -> mediaItemsAdded++ + else -> mediaItemsUpToDate++ + } } } @@ -91,7 +96,7 @@ class FolderScanner(var ctx: Context) { } } - private fun scanLibraryItemFolder(itemFolder:DocumentFile, localFolder:LocalFolder, existingItem:LocalLibraryItem?, forceAudioProbe:Boolean):ItemScanResult { + private fun scanLibraryItemFolder(itemFolder:DocumentFile, filesInFolder:List, localFolder:LocalFolder, existingItem:LocalLibraryItem?, forceAudioProbe:Boolean):ItemScanResult { val itemFolderName = itemFolder.name ?: "" val itemId = getLocalLibraryItemId(itemFolder.id) @@ -106,8 +111,6 @@ class FolderScanner(var ctx: Context) { var coverContentUrl:String? = null var coverAbsolutePath:String? = null - val filesInFolder = itemFolder.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4", "application/octet-stream")) - val existingLocalFilesRemoved = existingLocalFiles.filter { elf -> filesInFolder.find { fif -> DeviceManager.getBase64Id(fif.id) == elf.id } == null // File was not found in media item folder } diff --git a/android/app/src/main/java/com/audiobookshelf/app/managers/DownloadItemManager.kt b/android/app/src/main/java/com/audiobookshelf/app/managers/DownloadItemManager.kt index 03c1506f..5a6e0fdd 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/managers/DownloadItemManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/managers/DownloadItemManager.kt @@ -22,10 +22,11 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import java.util.* class DownloadItemManager(var downloadManager:DownloadManager, private var folderScanner: FolderScanner, var mainActivity: MainActivity, private var clientEventEmitter:DownloadEventEmitter) { val tag = "DownloadItemManager" - private val maxSimultaneousDownloads = 1 + private val maxSimultaneousDownloads = 3 private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature()) enum class DownloadCheckStatus { @@ -188,7 +189,8 @@ class DownloadItemManager(var downloadManager:DownloadManager, private var folde // Rename to fix appended .mp3 on m4b/m4a files // REF: https://github.com/anggrayudi/SimpleStorage/issues/94 - if (resultDocFile.name?.endsWith(".m4b.mp3") == true || resultDocFile.name?.endsWith(".m4a.mp3") == true) { + val docNameLowerCase = resultDocFile.name?.lowercase(Locale.getDefault()) ?: "" + if (docNameLowerCase.endsWith(".m4b.mp3")|| docNameLowerCase.endsWith(".m4a.mp3")) { resultDocFile.renameTo(downloadItemPart.filename) } diff --git a/android/app/src/main/java/com/audiobookshelf/app/models/DownloadItem.kt b/android/app/src/main/java/com/audiobookshelf/app/models/DownloadItem.kt index ca93b6e4..9f949cfd 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/models/DownloadItem.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/models/DownloadItem.kt @@ -32,7 +32,7 @@ data class DownloadItem( for (it in downloadItemParts) { if (!it.completed && it.downloadId == null) { itemParts.add(it) - if (itemParts.size > limit) break + if (itemParts.size >= limit) break } } diff --git a/android/gradlew b/android/gradlew old mode 100644 new mode 100755 diff --git a/assets/fonts.css b/assets/fonts.css index 8241ff6b..1c2cf48d 100644 --- a/assets/fonts.css +++ b/assets/fonts.css @@ -48,25 +48,6 @@ font-size: 1.5rem; } -@font-face { - font-family: 'Gentium Book Basic'; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url(/fonts/GentiumBookBasic.woff2) format('woff2'); - unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; -} - -/* latin */ -@font-face { - font-family: 'Gentium Book Basic'; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url(/fonts/GentiumBookBasic.woff2) format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; -} - /* cyrillic-ext */ @font-face { diff --git a/components/app/Appbar.vue b/components/app/Appbar.vue index 77a20543..c91311c4 100644 --- a/components/app/Appbar.vue +++ b/components/app/Appbar.vue @@ -10,7 +10,7 @@

-

{{ currentLibraryName }}

+

{{ currentLibraryName }}

diff --git a/components/app/AudioPlayerContainer.vue b/components/app/AudioPlayerContainer.vue index 060a2878..48d0be6d 100644 --- a/components/app/AudioPlayerContainer.vue +++ b/components/app/AudioPlayerContainer.vue @@ -43,6 +43,9 @@ export default { bookmarks() { if (!this.serverLibraryItemId) return [] return this.$store.getters['user/getUserBookmarksForItem'](this.serverLibraryItemId) + }, + isIos() { + return this.$platform === 'ios' } }, methods: { @@ -188,6 +191,7 @@ export default { const libraryItemId = payload.libraryItemId const episodeId = payload.episodeId const startTime = payload.startTime + const startWhenReady = !payload.paused // When playing local library item and can also play this item from the server // then store the server library item id so it can be used if a cast is made @@ -223,7 +227,7 @@ export default { } console.log('Called playLibraryItem', libraryItemId) - const preparePayload = { libraryItemId, episodeId, playWhenReady: true, playbackRate } + const preparePayload = { libraryItemId, episodeId, playWhenReady: startWhenReady, playbackRate } if (startTime !== undefined && startTime !== null) preparePayload.startTime = startTime AbsAudioPlayer.prepareLibraryItem(preparePayload) .then((data) => { @@ -268,9 +272,13 @@ export default { this.notifyOnReady() }, notifyOnReady() { + // TODO: iOS opens last active playback session on app launch. Should be consistent with Android + if (!this.isIos) return + // If settings aren't loaded yet, native player will receive incorrect settings console.log('Notify on ready... settingsLoaded:', this.settingsLoaded, 'isReady:', this.isReady) - if (this.settingsLoaded && this.isReady) { + if (this.settingsLoaded && this.isReady && this.$store.state.isFirstAudioLoad) { + this.$store.commit('setIsFirstAudioLoad', false) // Only run this once on app launch AbsAudioPlayer.onReady() } } diff --git a/components/bookshelf/Shelf.vue b/components/bookshelf/Shelf.vue index a95b30f2..367850fc 100644 --- a/components/bookshelf/Shelf.vue +++ b/components/bookshelf/Shelf.vue @@ -13,7 +13,7 @@ -
+

{{ label }}

diff --git a/components/cards/LazyBookCard.vue b/components/cards/LazyBookCard.vue index 789ee884..e6b6176f 100644 --- a/components/cards/LazyBookCard.vue +++ b/components/cards/LazyBookCard.vue @@ -18,7 +18,7 @@
-

{{ title }}

+

{{ title }}

@@ -26,11 +26,11 @@
-

{{ titleCleaned }}

+

{{ titleCleaned }}

-

{{ authorCleaned }}

+

{{ authorCleaned }}

diff --git a/components/cards/LazyCollectionCard.vue b/components/cards/LazyCollectionCard.vue index f76d0540..85e6b5b2 100644 --- a/components/cards/LazyCollectionCard.vue +++ b/components/cards/LazyCollectionCard.vue @@ -5,7 +5,7 @@
-
+

{{ title }}

diff --git a/components/cards/LazyPlaylistCard.vue b/components/cards/LazyPlaylistCard.vue index b3a1d18a..44448f70 100644 --- a/components/cards/LazyPlaylistCard.vue +++ b/components/cards/LazyPlaylistCard.vue @@ -4,7 +4,7 @@
-
+

{{ title }}

diff --git a/components/cards/LazySeriesCard.vue b/components/cards/LazySeriesCard.vue index f19aa574..7fdd4548 100644 --- a/components/cards/LazySeriesCard.vue +++ b/components/cards/LazySeriesCard.vue @@ -8,7 +8,7 @@

{{ title }}

-
+

{{ title }}

diff --git a/components/covers/BookCover.vue b/components/covers/BookCover.vue index a23fd83f..4f4c6238 100644 --- a/components/covers/BookCover.vue +++ b/components/covers/BookCover.vue @@ -1,14 +1,14 @@ @@ -48,7 +48,8 @@ export default { }, bookCoverAspectRatio: Number, downloadCover: String, - raw: Boolean + raw: Boolean, + noBg: Boolean }, data() { return { @@ -156,7 +157,7 @@ export default { this.$nextTick(() => { this.imageReady = true }) - if (this.$refs.cover && this.cover !== this.placeholderUrl) { + if (!this.noBg && this.$refs.cover && this.cover !== this.placeholderUrl) { var { naturalWidth, naturalHeight } = this.$refs.cover var aspectRatio = naturalHeight / naturalWidth var arDiff = Math.abs(aspectRatio - this.bookCoverAspectRatio) diff --git a/components/covers/CollectionCover.vue b/components/covers/CollectionCover.vue index 403024eb..879bddad 100644 --- a/components/covers/CollectionCover.vue +++ b/components/covers/CollectionCover.vue @@ -15,7 +15,7 @@
-

Empty Collection

+

Empty Collection

diff --git a/components/covers/GroupCover.vue b/components/covers/GroupCover.vue index 8af50fd0..d173b7fc 100644 --- a/components/covers/GroupCover.vue +++ b/components/covers/GroupCover.vue @@ -134,7 +134,7 @@ export default { var innerP = document.createElement('p') innerP.textContent = this.name - innerP.className = 'text-sm font-book text-white' + innerP.className = 'text-smtext-white' imgdiv.appendChild(innerP) return imgdiv diff --git a/components/covers/PreviewCover.vue b/components/covers/PreviewCover.vue index fac0271a..aab610de 100644 --- a/components/covers/PreviewCover.vue +++ b/components/covers/PreviewCover.vue @@ -14,7 +14,7 @@
-

Invalid Cover

+

Invalid Cover

diff --git a/components/home/BookshelfToolbar.vue b/components/home/BookshelfToolbar.vue index 63c0d2ec..148bc71a 100644 --- a/components/home/BookshelfToolbar.vue +++ b/components/home/BookshelfToolbar.vue @@ -5,8 +5,8 @@ arrow_back -

{{ totalEntities }} {{ entityTitle }}

-

{{ selectedSeriesName }} ({{ totalEntities }})

+

{{ totalEntities }} {{ entityTitle }}

+

{{ selectedSeriesName }} ({{ totalEntities }})

{{ !bookshelfListView ? 'view_list' : 'grid_view' }}
-
+