├── .devcontainer ├── Dockerfile ├── dev.js ├── devcontainer.json └── post-create.sh ├── .dockerignore ├── .editorconfig ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug.yaml │ ├── config.yml │ └── feature.yml ├── pull_request_template.md └── workflows │ ├── apply_comments.yaml │ ├── close-issues-on-release.yml │ ├── close_blank_issues.yaml │ ├── codeql.yml │ ├── component-tests.yml │ ├── docker-build.yml │ ├── i18n-integration.yml │ ├── integration-test.yml │ ├── lint-openapi.yml │ ├── notify-abs-windows.yml │ └── unit-tests.yml ├── .gitignore ├── .prettierrc ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── Dockerfile ├── LICENSE ├── build ├── debian │ ├── DEBIAN │ │ ├── control │ │ ├── postinst │ │ ├── preinst │ │ └── prerm │ ├── etc │ │ └── default │ │ │ └── .gitkeep │ ├── lib │ │ └── systemd │ │ │ └── system │ │ │ └── audiobookshelf.service │ └── usr │ │ ├── lib │ │ └── .gitkeep │ │ └── share │ │ └── .gitkeep └── linuxpackager ├── client ├── assets │ ├── absicons.css │ ├── app.css │ ├── defaultStyles.css │ ├── draggable.css │ ├── ebooks │ │ ├── basic.js │ │ ├── htmlParser.js │ │ └── mobi.js │ ├── fonts.css │ ├── tailwind.css │ ├── transitions.css │ └── trix.css ├── components │ ├── app │ │ ├── Appbar.vue │ │ ├── BookShelfCategorized.vue │ │ ├── BookShelfRow.vue │ │ ├── BookShelfToolbar.vue │ │ ├── ConfigSideNav.vue │ │ ├── LazyBookshelf.vue │ │ ├── MediaPlayerContainer.vue │ │ ├── SettingsContent.vue │ │ └── SideRail.vue │ ├── cards │ │ ├── AuthorCard.vue │ │ ├── AuthorSearchCard.vue │ │ ├── BookMatchCard.vue │ │ ├── EpisodeSearchCard.vue │ │ ├── GenreSearchCard.vue │ │ ├── GroupCard.vue │ │ ├── ItemSearchCard.vue │ │ ├── ItemTaskRunningCard.vue │ │ ├── ItemUploadCard.vue │ │ ├── LazyBookCard.vue │ │ ├── LazyCollectionCard.vue │ │ ├── LazyPlaylistCard.vue │ │ ├── LazySeriesCard.vue │ │ ├── NarratorCard.vue │ │ ├── NarratorSearchCard.vue │ │ ├── NotificationCard.vue │ │ ├── PodcastFeedSummaryCard.vue │ │ ├── SeriesSearchCard.vue │ │ └── TagSearchCard.vue │ ├── content │ │ └── LibraryItemDetails.vue │ ├── controls │ │ ├── FilterSelect.vue │ │ ├── GlobalSearch.vue │ │ ├── LibraryFilterSelect.vue │ │ ├── LibrarySortSelect.vue │ │ ├── PlaybackSpeedControl.vue │ │ ├── SortSelect.vue │ │ └── VolumeControl.vue │ ├── covers │ │ ├── AuthorImage.vue │ │ ├── BookCover.vue │ │ ├── CollectionCover.vue │ │ ├── GroupCover.vue │ │ ├── PlaylistCover.vue │ │ └── PreviewCover.vue │ ├── modals │ │ ├── AccountModal.vue │ │ ├── AddCustomMetadataProviderModal.vue │ │ ├── AudioFileDataModal.vue │ │ ├── BackupScheduleModal.vue │ │ ├── BatchQuickMatchModel.vue │ │ ├── BookmarksModal.vue │ │ ├── ChaptersModal.vue │ │ ├── Dialog.vue │ │ ├── EditSeriesInputInnerModal.vue │ │ ├── ListeningSessionModal.vue │ │ ├── Modal.vue │ │ ├── PlayerSettingsModal.vue │ │ ├── RawCoverPreviewModal.vue │ │ ├── ShareModal.vue │ │ ├── SleepTimerModal.vue │ │ ├── UploadImageModal.vue │ │ ├── authors │ │ │ └── EditModal.vue │ │ ├── bookmarks │ │ │ └── BookmarkItem.vue │ │ ├── changelog │ │ │ └── ViewModal.vue │ │ ├── collections │ │ │ ├── AddCreateModal.vue │ │ │ ├── CollectionItem.vue │ │ │ └── EditModal.vue │ │ ├── emails │ │ │ ├── EReaderDeviceModal.vue │ │ │ └── UserEReaderDeviceModal.vue │ │ ├── item │ │ │ ├── EditModal.vue │ │ │ └── tabs │ │ │ │ ├── Chapters.vue │ │ │ │ ├── Cover.vue │ │ │ │ ├── Details.vue │ │ │ │ ├── Episodes.vue │ │ │ │ ├── Files.vue │ │ │ │ ├── Match.vue │ │ │ │ ├── Schedule.vue │ │ │ │ └── Tools.vue │ │ ├── libraries │ │ │ ├── EditLibrary.vue │ │ │ ├── EditModal.vue │ │ │ ├── LazyFolderChooser.vue │ │ │ ├── LibraryScannerSettings.vue │ │ │ ├── LibrarySettings.vue │ │ │ ├── LibraryTools.vue │ │ │ └── ScheduleScan.vue │ │ ├── notification │ │ │ └── NotificationEditModal.vue │ │ ├── player │ │ │ ├── QueueItemRow.vue │ │ │ └── QueueItemsModal.vue │ │ ├── playlists │ │ │ ├── AddCreateModal.vue │ │ │ ├── EditModal.vue │ │ │ └── UserPlaylistItem.vue │ │ ├── podcast │ │ │ ├── EditEpisode.vue │ │ │ ├── EpisodeFeed.vue │ │ │ ├── NewModal.vue │ │ │ ├── OpmlFeedsModal.vue │ │ │ ├── RemoveEpisode.vue │ │ │ ├── ViewEpisode.vue │ │ │ └── tabs │ │ │ │ ├── EpisodeDetails.vue │ │ │ │ └── EpisodeMatch.vue │ │ └── rssfeed │ │ │ ├── OpenCloseModal.vue │ │ │ └── ViewFeedModal.vue │ ├── player │ │ ├── PlayerPlaybackControls.vue │ │ ├── PlayerTrackBar.vue │ │ └── PlayerUi.vue │ ├── prompt │ │ ├── Confirm.vue │ │ └── Dialog.vue │ ├── readers │ │ ├── ComicReader.vue │ │ ├── EpubReader.vue │ │ ├── MobiReader.vue │ │ ├── PdfReader.vue │ │ └── Reader.vue │ ├── stats │ │ ├── DailyListeningChart.vue │ │ ├── Heatmap.vue │ │ ├── PreviewIcons.vue │ │ ├── YearInReview.vue │ │ ├── YearInReviewBanner.vue │ │ ├── YearInReviewServer.vue │ │ └── YearInReviewShort.vue │ ├── tables │ │ ├── AudioTracksTableRow.vue │ │ ├── BackupsTable.vue │ │ ├── ChaptersTable.vue │ │ ├── CollectionBooksTable.vue │ │ ├── CustomMetadataProviderTable.vue │ │ ├── EbookFilesTable.vue │ │ ├── EbookFilesTableRow.vue │ │ ├── LibraryFilesTable.vue │ │ ├── LibraryFilesTableRow.vue │ │ ├── PlaylistItemsTable.vue │ │ ├── TracksTable.vue │ │ ├── UploadedFilesTable.vue │ │ ├── UsersTable.vue │ │ ├── collection │ │ │ └── BookTableRow.vue │ │ ├── library │ │ │ ├── LibrariesTable.vue │ │ │ └── LibraryItem.vue │ │ ├── playlist │ │ │ └── ItemTableRow.vue │ │ └── podcast │ │ │ ├── DownloadQueueTable.vue │ │ │ ├── LazyEpisodeRow.vue │ │ │ └── LazyEpisodesTable.vue │ ├── ui │ │ ├── Btn.vue │ │ ├── Checkbox.vue │ │ ├── ContextMenuDropdown.vue │ │ ├── Dropdown.vue │ │ ├── EditableText.vue │ │ ├── FileInput.vue │ │ ├── IconBtn.vue │ │ ├── InputDropdown.vue │ │ ├── LibrariesDropdown.vue │ │ ├── LibraryIcon.vue │ │ ├── LoadingIndicator.vue │ │ ├── MediaIconPicker.vue │ │ ├── MultiSelect.vue │ │ ├── MultiSelectDropdown.vue │ │ ├── MultiSelectQueryInput.vue │ │ ├── QueryInput.vue │ │ ├── RangeInput.vue │ │ ├── ReadIconBtn.vue │ │ ├── RichTextEditor.vue │ │ ├── SelectInput.vue │ │ ├── TextInput.vue │ │ ├── TextInputWithLabel.vue │ │ ├── TextareaInput.vue │ │ ├── TextareaWithLabel.vue │ │ ├── TimePicker.vue │ │ ├── ToggleBtns.vue │ │ ├── ToggleSwitch.vue │ │ ├── Tooltip.vue │ │ └── VueTrix.vue │ └── widgets │ │ ├── AbridgedIndicator.vue │ │ ├── Alert.vue │ │ ├── AlreadyInLibraryIndicator.vue │ │ ├── BookDetailsEdit.vue │ │ ├── CoverSizeWidget.vue │ │ ├── CronExpressionBuilder.vue │ │ ├── EncoderOptionsCard.vue │ │ ├── ExplicitIndicator.vue │ │ ├── ItemSlider.vue │ │ ├── LoadingSpinner.vue │ │ ├── MoreMenu.vue │ │ ├── NotificationWidget.vue │ │ ├── OnlineIndicator.vue │ │ ├── PodcastDetailsEdit.vue │ │ ├── PodcastTypeIndicator.vue │ │ ├── RssFeedMetadataBuilder.vue │ │ └── SeriesInputWidget.vue ├── cypress.config.js ├── cypress │ ├── fixtures │ │ └── images │ │ │ ├── book_placeholder.jpg │ │ │ ├── cover1.jpg │ │ │ └── cover2.jpg │ ├── support │ │ ├── commands.js │ │ ├── component-index.html │ │ └── component.js │ └── tests │ │ ├── components │ │ └── cards │ │ │ ├── AuthorCard.cy.js │ │ │ ├── ItemSlider.cy.js │ │ │ ├── LazyBookCard.cy.js │ │ │ ├── LazySeriesCard.cy.js │ │ │ └── NarratorCard.cy.js │ │ └── utils │ │ └── ElapsedPrettyExtended.cy.js ├── layouts │ ├── blank.vue │ ├── default.vue │ └── error.vue ├── middleware │ └── authenticated.js ├── mixins │ ├── bookshelfCardsHelpers.js │ ├── menuKeyboardNavigation.js │ └── uploadHelpers.js ├── nuxt.config.js ├── package-lock.json ├── package.json ├── pages │ ├── account.vue │ ├── audiobook │ │ └── _id │ │ │ ├── chapters.vue │ │ │ ├── edit.vue │ │ │ └── manage.vue │ ├── author │ │ └── _id.vue │ ├── batch │ │ └── index.vue │ ├── collection │ │ └── _id.vue │ ├── config.vue │ ├── config │ │ ├── authentication.vue │ │ ├── backups.vue │ │ ├── email.vue │ │ ├── index.vue │ │ ├── item-metadata-utils │ │ │ ├── custom-metadata-providers.vue │ │ │ ├── genres.vue │ │ │ ├── index.vue │ │ │ └── tags.vue │ │ ├── libraries.vue │ │ ├── log.vue │ │ ├── notifications.vue │ │ ├── rss-feeds.vue │ │ ├── sessions.vue │ │ ├── stats.vue │ │ └── users │ │ │ ├── _id │ │ │ ├── index.vue │ │ │ └── sessions.vue │ │ │ └── index.vue │ ├── index.vue │ ├── item │ │ └── _id │ │ │ └── index.vue │ ├── library │ │ └── _library │ │ │ ├── bookshelf │ │ │ └── _id.vue │ │ │ ├── index.vue │ │ │ ├── narrators.vue │ │ │ ├── podcast │ │ │ ├── download-queue.vue │ │ │ ├── latest.vue │ │ │ └── search.vue │ │ │ ├── search.vue │ │ │ ├── series │ │ │ └── _id.vue │ │ │ └── stats.vue │ ├── login.vue │ ├── oops.vue │ ├── playlist │ │ └── _id.vue │ ├── share │ │ └── _slug.vue │ └── upload │ │ └── index.vue ├── players │ ├── AudioTrack.js │ ├── CastPlayer.js │ ├── LocalAudioPlayer.js │ ├── PlayerHandler.js │ └── castUtils.js ├── plugins │ ├── axios.js │ ├── chromecast.js │ ├── constants.js │ ├── i18n.js │ ├── init.client.js │ ├── toast.js │ ├── utils.js │ └── version.js ├── postcss.config.js ├── static │ ├── Logo.png │ ├── book_placeholder.jpg │ ├── favicon.ico │ ├── fonts │ │ ├── MaterialSymbolsRounded.woff2 │ │ ├── Source_Sans_Pro │ │ │ ├── OFL.txt │ │ │ ├── SourceSansPro-Light.ttf │ │ │ ├── SourceSansPro-Regular.ttf │ │ │ └── SourceSansPro-SemiBold.ttf │ │ ├── Ubuntu_Mono │ │ │ ├── UFL.txt │ │ │ └── UbuntuMono-Regular.ttf │ │ └── absicons │ │ │ ├── absicons.eot │ │ │ ├── absicons.svg │ │ │ ├── absicons.ttf │ │ │ └── absicons.woff │ ├── icon.svg │ ├── icon192.png │ ├── icon48.png │ ├── icon64.png │ ├── ios_icon.png │ ├── libarchive │ │ ├── wasm-gen │ │ │ ├── libarchive.js │ │ │ └── libarchive.wasm │ │ └── worker-bundle.js │ ├── libs │ │ └── marked │ │ │ ├── LICENSE │ │ │ └── index.js │ ├── robots.txt │ └── textures │ │ └── wood_default.jpg ├── store │ ├── globals.js │ ├── index.js │ ├── libraries.js │ ├── scanners.js │ ├── tasks.js │ ├── user.js │ └── users.js └── strings │ ├── ar.json │ ├── be.json │ ├── bg.json │ ├── bn.json │ ├── ca.json │ ├── cs.json │ ├── da.json │ ├── de.json │ ├── en-us.json │ ├── es.json │ ├── et.json │ ├── fi.json │ ├── fr.json │ ├── gu.json │ ├── he.json │ ├── hi.json │ ├── hr.json │ ├── hu.json │ ├── it.json │ ├── ja.json │ ├── lt.json │ ├── nl.json │ ├── no.json │ ├── pl.json │ ├── pt-br.json │ ├── ro.json │ ├── ru.json │ ├── sk.json │ ├── sl.json │ ├── sv.json │ ├── tr.json │ ├── uk.json │ ├── vi-vn.json │ ├── zh-cn.json │ └── zh-tw.json ├── custom-metadata-provider-specification.yaml ├── docker-compose.yml ├── docker-template.xml ├── docs ├── README.md ├── controllers │ ├── AuthorController.yaml │ ├── EmailController.yaml │ ├── LibraryController.yaml │ ├── NotificationController.yaml │ ├── PodcastController.yaml │ └── SeriesController.yaml ├── objects │ ├── Folder.yaml │ ├── Library.yaml │ ├── LibraryItem.yaml │ ├── Notification.yaml │ ├── entities │ │ ├── Author.yaml │ │ ├── PodcastEpisode.yaml │ │ └── Series.yaml │ ├── files │ │ ├── AudioFile.yaml │ │ ├── AudioTrack.yaml │ │ └── EBookFile.yaml │ ├── mediaTypes │ │ ├── Book.yaml │ │ ├── Podcast.yaml │ │ └── media.yaml │ ├── metadata │ │ ├── AudioMetaTags.yaml │ │ ├── BookMetadata.yaml │ │ ├── FileMetadata.yaml │ │ └── PodcastMetadata.yaml │ └── settings │ │ └── EmailSettings.yaml ├── openapi.json ├── root.yaml └── schemas.yaml ├── images ├── DemoLibrary.png ├── LibraryStreamSquare.png └── banner.svg ├── index.js ├── package-lock.json ├── package.json ├── prod.js ├── readme.md ├── server ├── Auth.js ├── Database.js ├── Logger.js ├── Server.js ├── SocketAuthority.js ├── Watcher.js ├── controllers │ ├── AuthorController.js │ ├── BackupController.js │ ├── CacheController.js │ ├── CollectionController.js │ ├── CustomMetadataProviderController.js │ ├── EmailController.js │ ├── FileSystemController.js │ ├── LibraryController.js │ ├── LibraryItemController.js │ ├── MeController.js │ ├── MiscController.js │ ├── NotificationController.js │ ├── PlaylistController.js │ ├── PodcastController.js │ ├── RSSFeedController.js │ ├── SearchController.js │ ├── SeriesController.js │ ├── SessionController.js │ ├── ShareController.js │ ├── StatsController.js │ ├── ToolsController.js │ └── UserController.js ├── finders │ ├── AuthorFinder.js │ ├── BookFinder.js │ └── PodcastFinder.js ├── libs │ ├── archiver │ │ ├── LICENSE │ │ ├── archiverUtils │ │ │ ├── balancedMatch │ │ │ │ ├── LICENSE │ │ │ │ └── index.js │ │ │ ├── braceExpansion │ │ │ │ ├── LICENSE │ │ │ │ └── index.js │ │ │ ├── file.js │ │ │ ├── fsRealpath │ │ │ │ ├── LICENSE │ │ │ │ ├── index.js │ │ │ │ └── old.js │ │ │ ├── glob │ │ │ │ ├── LICENSE │ │ │ │ ├── common.js │ │ │ │ ├── index.js │ │ │ │ └── sync.js │ │ │ ├── index.js │ │ │ ├── inflight │ │ │ │ ├── LICENSE │ │ │ │ └── index.js │ │ │ ├── lazystream │ │ │ │ ├── LICENSE │ │ │ │ ├── index.js │ │ │ │ └── readable-stream │ │ │ │ │ ├── lib │ │ │ │ │ ├── _stream_duplex.js │ │ │ │ │ ├── _stream_passthrough.js │ │ │ │ │ ├── _stream_readable.js │ │ │ │ │ ├── _stream_transform.js │ │ │ │ │ ├── _stream_writable.js │ │ │ │ │ └── internal │ │ │ │ │ │ └── streams │ │ │ │ │ │ ├── BufferList.js │ │ │ │ │ │ ├── destroy.js │ │ │ │ │ │ ├── stream-browser.js │ │ │ │ │ │ └── stream.js │ │ │ │ │ ├── passthrough.js │ │ │ │ │ └── readable.js │ │ │ ├── lodash.difference │ │ │ │ ├── LICENSE │ │ │ │ └── index.js │ │ │ ├── lodash.flatten │ │ │ │ ├── LICENSE │ │ │ │ └── index.js │ │ │ ├── lodash.isplainobject │ │ │ │ ├── LICENSE │ │ │ │ └── index.js │ │ │ ├── lodash.union │ │ │ │ ├── LICENSE │ │ │ │ └── index.js │ │ │ ├── minimatch │ │ │ │ ├── LICENSE │ │ │ │ └── index.js │ │ │ ├── readableStream │ │ │ │ ├── LICENSE │ │ │ │ ├── _stream_duplex.js │ │ │ │ ├── _stream_passthrough.js │ │ │ │ ├── _stream_readable.js │ │ │ │ ├── _stream_transform.js │ │ │ │ ├── _stream_writable.js │ │ │ │ ├── index.js │ │ │ │ ├── internal │ │ │ │ │ ├── streams │ │ │ │ │ │ ├── add-abort-signal.js │ │ │ │ │ │ ├── buffer_list.js │ │ │ │ │ │ ├── compose.js │ │ │ │ │ │ ├── destroy.js │ │ │ │ │ │ ├── duplex.js │ │ │ │ │ │ ├── duplexify.js │ │ │ │ │ │ ├── end-of-stream.js │ │ │ │ │ │ ├── from.js │ │ │ │ │ │ ├── lazy_transform.js │ │ │ │ │ │ ├── legacy.js │ │ │ │ │ │ ├── operators.js │ │ │ │ │ │ ├── passthrough.js │ │ │ │ │ │ ├── pipeline.js │ │ │ │ │ │ ├── readable.js │ │ │ │ │ │ ├── state.js │ │ │ │ │ │ ├── transform.js │ │ │ │ │ │ ├── utils.js │ │ │ │ │ │ └── writable.js │ │ │ │ │ └── validators.js │ │ │ │ ├── ours │ │ │ │ │ ├── browser.js │ │ │ │ │ ├── errors.js │ │ │ │ │ ├── primordials.js │ │ │ │ │ └── util.js │ │ │ │ ├── stream.js │ │ │ │ └── stream │ │ │ │ │ └── promises.js │ │ │ ├── safeBuffer │ │ │ │ ├── LICENSE │ │ │ │ └── index.js │ │ │ ├── stringDecoder │ │ │ │ ├── LICENSE │ │ │ │ └── index.js │ │ │ └── wrappy │ │ │ │ ├── LICENSE │ │ │ │ └── index.js │ │ ├── buffer-crc32 │ │ │ ├── LICENSE │ │ │ └── index.js │ │ ├── compress-commons │ │ │ ├── LICENSE │ │ │ ├── archivers │ │ │ │ ├── archive-entry.js │ │ │ │ ├── archive-output-stream.js │ │ │ │ └── zip │ │ │ │ │ ├── constants.js │ │ │ │ │ ├── general-purpose-bit.js │ │ │ │ │ ├── unix-stat.js │ │ │ │ │ ├── util.js │ │ │ │ │ ├── zip-archive-entry.js │ │ │ │ │ └── zip-archive-output-stream.js │ │ │ ├── index.js │ │ │ └── util │ │ │ │ └── index.js │ │ ├── crc32-stream │ │ │ ├── LICENSE │ │ │ ├── crc32-stream.js │ │ │ ├── deflate-crc32-stream.js │ │ │ └── index.js │ │ ├── crc32 │ │ │ ├── LICENSE │ │ │ └── index.js │ │ ├── index.js │ │ ├── lib │ │ │ ├── core.js │ │ │ ├── error.js │ │ │ └── plugins │ │ │ │ ├── json.js │ │ │ │ └── zip.js │ │ ├── normalize-path │ │ │ ├── LICENSE │ │ │ └── index.js │ │ ├── readdir-glob │ │ │ ├── LICENSE │ │ │ └── index.js │ │ └── zip-stream │ │ │ ├── LICENSE │ │ │ └── index.js │ ├── async │ │ ├── LICENSE │ │ └── index.js │ ├── bcryptjs │ │ ├── LICENSE │ │ └── index.js │ ├── busboy │ │ ├── LICENSE │ │ ├── index.js │ │ ├── types │ │ │ ├── multipart.js │ │ │ └── urlencoded.js │ │ └── utils.js │ ├── commandLineArgs │ │ └── index.js │ ├── dateAndTime │ │ ├── LICENSE │ │ └── index.js │ ├── expressFileupload │ │ ├── LICENSE │ │ ├── fileFactory.js │ │ ├── index.js │ │ ├── isEligibleRequest.js │ │ ├── memHandler.js │ │ ├── processMultipart.js │ │ ├── processNested.js │ │ ├── tempFileHandler.js │ │ ├── uploadtimer.js │ │ └── utilities.js │ ├── fastSort │ │ ├── LICENSE │ │ └── index.js │ ├── fluentFfmpeg │ │ ├── LICENSE │ │ ├── capabilities.js │ │ ├── ffprobe.js │ │ ├── index.d.ts │ │ ├── index.js │ │ ├── options │ │ │ ├── audio.js │ │ │ ├── custom.js │ │ │ ├── inputs.js │ │ │ ├── misc.js │ │ │ ├── output.js │ │ │ ├── video.js │ │ │ └── videosize.js │ │ ├── presets │ │ │ ├── divx.js │ │ │ ├── flashvideo.js │ │ │ └── podcast.js │ │ ├── processor.js │ │ ├── recipes.js │ │ └── utils.js │ ├── fsExtra │ │ ├── LICENSE │ │ ├── copy │ │ │ ├── copy-sync.js │ │ │ ├── copy.js │ │ │ └── index.js │ │ ├── empty │ │ │ └── index.js │ │ ├── ensure │ │ │ ├── file.js │ │ │ ├── index.js │ │ │ ├── link.js │ │ │ ├── symlink-paths.js │ │ │ ├── symlink-type.js │ │ │ └── symlink.js │ │ ├── fs │ │ │ └── index.js │ │ ├── index.js │ │ ├── mkdirs │ │ │ ├── index.js │ │ │ ├── make-dir.js │ │ │ └── utils.js │ │ ├── move │ │ │ ├── index.js │ │ │ ├── move-sync.js │ │ │ └── move.js │ │ ├── path-exists │ │ │ └── index.js │ │ ├── remove │ │ │ ├── index.js │ │ │ └── rimraf.js │ │ └── util │ │ │ ├── stat.js │ │ │ └── utimes.js │ ├── imageType │ │ ├── LICENSE │ │ ├── fileType.js │ │ └── index.js │ ├── isexe │ │ ├── LICENSE │ │ ├── index.js │ │ ├── mode.js │ │ └── windows.js │ ├── jsonwebtoken │ │ ├── LICENSE │ │ ├── decode.js │ │ ├── index.js │ │ ├── lib │ │ │ ├── JsonWebTokenError.js │ │ │ ├── NotBeforeError.js │ │ │ ├── TokenExpiredError.js │ │ │ └── timespan.js │ │ ├── sign.js │ │ └── verify.js │ ├── jwa │ │ ├── LICENSE │ │ ├── buffer-equal-constant-time │ │ │ ├── LICENSE │ │ │ └── index.js │ │ ├── ecdsa-sig-formatter │ │ │ ├── LICENSE │ │ │ ├── index.js │ │ │ └── param-bytes-for-alg.js │ │ └── index.js │ ├── jws │ │ ├── LICENSE │ │ ├── index.js │ │ └── lib │ │ │ ├── data-stream.js │ │ │ ├── sign-stream.js │ │ │ ├── tostring.js │ │ │ └── verify-stream.js │ ├── libarchive │ │ ├── LICENSE │ │ ├── archive.js │ │ ├── libarchiveWorker.js │ │ ├── wasm-libarchive.js │ │ └── wasm-module.js │ ├── lodash.once │ │ ├── LICENSE │ │ └── index.js │ ├── memorystore │ │ ├── LICENSE │ │ └── index.js │ ├── nodeCron │ │ ├── LICENSE │ │ ├── background-scheduled-task │ │ │ ├── daemon.js │ │ │ └── index.js │ │ ├── convert-expression │ │ │ ├── asterisk-to-range-conversion.js │ │ │ ├── index.js │ │ │ ├── month-names-conversion.js │ │ │ ├── range-conversion.js │ │ │ ├── step-values-conversion.js │ │ │ └── week-day-names-conversion.js │ │ ├── index.js │ │ ├── pattern-validation.js │ │ ├── scheduled-task.js │ │ ├── scheduler.js │ │ ├── storage.js │ │ ├── task.js │ │ └── time-matcher.js │ ├── nodeFfprobe │ │ ├── LICENSE │ │ └── index.js │ ├── nodeStreamZip │ │ ├── LICENSE │ │ └── index.js │ ├── passportLocal │ │ ├── LICENSE │ │ ├── index.js │ │ └── strategy.js │ ├── readChunk │ │ ├── LICENSE │ │ ├── index.js │ │ ├── pify.js │ │ └── withOpenFile.js │ ├── recursiveReaddirAsync │ │ ├── LICENSE │ │ └── index.js │ ├── requestIp │ │ ├── LICENSE │ │ ├── index.js │ │ └── isJs.js │ ├── rss │ │ ├── LICENSE │ │ └── index.js │ ├── sanitizeHtml │ │ ├── LICENSE │ │ └── index.js │ ├── streamsearch │ │ ├── LICENSE │ │ └── index.js │ ├── uaParser │ │ ├── LICENSE │ │ └── index.js │ ├── umzug │ │ ├── LICENSE │ │ ├── index.js │ │ ├── storage │ │ │ ├── contract.js │ │ │ ├── index.js │ │ │ ├── json.js │ │ │ ├── memory.js │ │ │ ├── mongodb.js │ │ │ └── sequelize.js │ │ ├── templates.js │ │ ├── types.js │ │ └── umzug.js │ ├── universalify │ │ ├── LICENSE │ │ └── index.js │ ├── watcher │ │ ├── LICENSE │ │ ├── aborter │ │ │ ├── controller.js │ │ │ └── signal.js │ │ ├── are-shallow-equal.js │ │ ├── atomically │ │ │ ├── consts.js │ │ │ ├── index.js │ │ │ └── utils │ │ │ │ ├── attemptify.js │ │ │ │ ├── fs.js │ │ │ │ ├── fs_handlers.js │ │ │ │ ├── lang.js │ │ │ │ ├── retryify.js │ │ │ │ ├── retryify_queue.js │ │ │ │ ├── scheduler.js │ │ │ │ └── temp.js │ │ ├── constants.js │ │ ├── debounce.js │ │ ├── enums.js │ │ ├── is-primitive.js │ │ ├── promise-concurrency-limiter.js │ │ ├── ripstat │ │ │ ├── consts.js │ │ │ ├── index.js │ │ │ └── stats.js │ │ ├── string-indexes.js │ │ ├── tiny-readdir.js │ │ ├── types.js │ │ ├── utils.js │ │ ├── watcher.js │ │ ├── watcher_handler.js │ │ ├── watcher_locker.js │ │ ├── watcher_locks_resolver.js │ │ ├── watcher_poller.js │ │ └── watcher_stats.js │ ├── which │ │ ├── LICENSE │ │ └── index.js │ └── xml │ │ ├── LICENSE │ │ ├── escapeForXML.js │ │ └── index.js ├── managers │ ├── AbMergeManager.js │ ├── ApiCacheManager.js │ ├── AudioMetadataManager.js │ ├── BackupManager.js │ ├── BinaryManager.js │ ├── CacheManager.js │ ├── CoverManager.js │ ├── CronManager.js │ ├── EmailManager.js │ ├── LogManager.js │ ├── MigrationManager.js │ ├── NotificationManager.js │ ├── PlaybackSessionManager.js │ ├── PodcastManager.js │ ├── RssFeedManager.js │ ├── ShareManager.js │ └── TaskManager.js ├── migrations │ ├── changelog.md │ ├── readme.md │ ├── v2.15.0-series-column-unique.js │ ├── v2.15.1-reindex-nocase.js │ ├── v2.15.2-index-creation.js │ ├── v2.17.0-uuid-replacement.js │ ├── v2.17.3-fk-constraints.js │ ├── v2.17.4-use-subfolder-for-oidc-redirect-uris.js │ ├── v2.17.5-remove-host-from-feed-urls.js │ ├── v2.17.6-share-add-isdownloadable.js │ ├── v2.17.7-add-indices.js │ ├── v2.19.1-copy-title-to-library-items.js │ ├── v2.19.4-improve-podcast-queries.js │ └── v2.20.0-improve-author-sort-queries.js ├── models │ ├── Author.js │ ├── Book.js │ ├── BookAuthor.js │ ├── BookSeries.js │ ├── Collection.js │ ├── CollectionBook.js │ ├── CustomMetadataProvider.js │ ├── Device.js │ ├── Feed.js │ ├── FeedEpisode.js │ ├── Library.js │ ├── LibraryFolder.js │ ├── LibraryItem.js │ ├── MediaItemShare.js │ ├── MediaProgress.js │ ├── PlaybackSession.js │ ├── Playlist.js │ ├── PlaylistMediaItem.js │ ├── Podcast.js │ ├── PodcastEpisode.js │ ├── Series.js │ ├── Setting.js │ └── User.js ├── objects │ ├── Backup.js │ ├── DailyLog.js │ ├── DeviceInfo.js │ ├── Notification.js │ ├── PlaybackSession.js │ ├── PodcastEpisodeDownload.js │ ├── Stream.js │ ├── Task.js │ ├── TrackProgressMonitor.js │ ├── files │ │ ├── AudioFile.js │ │ ├── AudioTrack.js │ │ ├── EBookFile.js │ │ └── LibraryFile.js │ ├── metadata │ │ ├── AudioMetaTags.js │ │ └── FileMetadata.js │ └── settings │ │ ├── EmailSettings.js │ │ ├── NotificationSettings.js │ │ └── ServerSettings.js ├── providers │ ├── Audible.js │ ├── AudiobookCovers.js │ ├── Audnexus.js │ ├── CustomProviderAdapter.js │ ├── FantLab.js │ ├── GoogleBooks.js │ ├── MusicBrainz.js │ ├── OpenLibrary.js │ └── iTunes.js ├── routers │ ├── ApiRouter.js │ ├── HlsRouter.js │ └── PublicRouter.js ├── scanner │ ├── AbsMetadataFileScanner.js │ ├── AudioFileScanner.js │ ├── BookScanner.js │ ├── LibraryItemScanData.js │ ├── LibraryItemScanner.js │ ├── LibraryScan.js │ ├── LibraryScanner.js │ ├── MediaProbeData.js │ ├── NfoFileScanner.js │ ├── OpfFileScanner.js │ ├── PodcastScanner.js │ ├── ScanLogger.js │ └── Scanner.js └── utils │ ├── areEquivalent.js │ ├── comicBookExtractors.js │ ├── constants.js │ ├── ffmpegHelpers.js │ ├── fileUtils.js │ ├── generators │ ├── abmetadataGenerator.js │ ├── hlsPlaylistGenerator.js │ └── opmlGenerator.js │ ├── globals.js │ ├── htmlEntities.js │ ├── htmlSanitizer.js │ ├── index.js │ ├── libraryHelpers.js │ ├── longTimeout.js │ ├── migrations │ ├── absMetadataMigration.js │ ├── dbMigration.js │ └── oldDbFiles.js │ ├── notifications.js │ ├── parsers │ ├── parseComicInfoMetadata.js │ ├── parseComicMetadata.js │ ├── parseEbookMetadata.js │ ├── parseEpubMetadata.js │ ├── parseFullName.js │ ├── parseNameString.js │ ├── parseNfoMetadata.js │ ├── parseOPML.js │ ├── parseOpfMetadata.js │ ├── parseOverdriveMediaMarkers.js │ └── parseSeriesString.js │ ├── podcastUtils.js │ ├── prober.js │ ├── profiler.js │ ├── queries │ ├── adminStats.js │ ├── authorFilters.js │ ├── libraryFilters.js │ ├── libraryItemFilters.js │ ├── libraryItemsBookFilters.js │ ├── libraryItemsPodcastFilters.js │ ├── seriesFilters.js │ └── userStats.js │ ├── scandir.js │ ├── stringifySequelizeQuery.js │ └── zipHelpers.js ├── tailwind.config.js └── test └── server ├── Logger.test.js ├── controllers └── LibraryItemController.test.js ├── finders └── BookFinder.test.js ├── managers ├── ApiCacheManager.test.js ├── BinaryManager.test.js ├── MigrationManager.test.js └── migrations │ ├── v1.0.0-migration.js │ ├── v1.1.0-migration.js │ ├── v1.10.0-migration.js │ └── v1.2.0-migration.js ├── migrations ├── v0.0.1-migration_example.js ├── v0.0.1-migration_example.test.js ├── v2.15.0-series-column-unique.test.js ├── v2.17.3-fk-constraints.test.js ├── v2.17.4-use-subfolder-for-oidc-redirect-uris.test.js ├── v2.17.5-remove-host-from-feed-urls.test.js ├── v2.17.6-share-add-isdownloadable.test.js ├── v2.19.1-copy-title-to-library-items.test.js ├── v2.19.4-improve-podcast-queries.test.js └── v2.20.0-improve-author-sort-queries.test.js ├── objects └── TrackProgressMonitor.test.js ├── providers └── Audible.test.js └── utils ├── ffmpegHelpers.test.js ├── fileUtils.test.js ├── parsers ├── parseNameString.test.js ├── parseNfoMetadata.test.js └── parseOpfMetadata.test.js ├── scandir.test.js └── stringifySequeslizeQuery.test.js /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 18, 16, 14, 18-bullseye, 16-bullseye, 14-bullseye, 18-buster, 16-buster, 14-buster 2 | ARG VARIANT=20 3 | FROM mcr.microsoft.com/devcontainers/javascript-node:0-${VARIANT} as base 4 | 5 | # Setup the node environment 6 | ENV NODE_ENV=development 7 | 8 | # Install additional OS packages. 9 | RUN apt-get update && \ 10 | DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends \ 11 | curl tzdata ffmpeg && \ 12 | rm -rf /var/lib/apt/lists/* 13 | -------------------------------------------------------------------------------- /.devcontainer/dev.js: -------------------------------------------------------------------------------- 1 | // Using port 3333 is important when running the client web app separately 2 | const Path = require('path') 3 | module.exports.config = { 4 | Port: 3333, 5 | ConfigPath: Path.resolve('config'), 6 | MetadataPath: Path.resolve('metadata'), 7 | FFmpegPath: '/usr/bin/ffmpeg', 8 | FFProbePath: '/usr/bin/ffprobe', 9 | SkipBinariesCheck: false 10 | } 11 | -------------------------------------------------------------------------------- /.devcontainer/post-create.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Mark the working directory as safe for use with git 4 | git config --global --add safe.directory $PWD 5 | 6 | # If there is no dev.js file, create it 7 | if [ ! -f dev.js ]; then 8 | cp .devcontainer/dev.js . 9 | fi 10 | 11 | # Update permissions for node_modules folders 12 | # https://code.visualstudio.com/remote/advancedcontainers/improve-performance#_use-a-targeted-named-volume 13 | if [ -d node_modules ]; then 14 | sudo chown $(id -u):$(id -g) node_modules 15 | fi 16 | 17 | if [ -d client/node_modules ]; then 18 | sudo chown $(id -u):$(id -g) client/node_modules 19 | fi 20 | 21 | # Install packages for the server 22 | if [ -f package.json ]; then 23 | npm ci 24 | fi 25 | 26 | # Install packages and build the client 27 | if [ -f client/package.json ]; then 28 | (cd client; npm ci; npm run generate) 29 | fi 30 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules 3 | npm-debug.log 4 | .git 5 | .gitignore 6 | /config 7 | /audiobooks 8 | /audiobooks2 9 | /media/ 10 | /metadata 11 | dev.js 12 | test/ 13 | /client/.nuxt/ 14 | /client/dist/ 15 | /dist/ 16 | /deploy/ -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=auto 3 | 4 | # Declare files that will always have CRLF line endings on checkout. 5 | .devcontainer/post-create.sh text eol=lf 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Discord 4 | url: https://discord.gg/HQgCbd6E75 5 | about: Ask questions, get help troubleshooting, and join the Abs community here. 6 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 10 | 11 | ## Brief summary 12 | 13 | 14 | 15 | ## Which issue is fixed? 16 | 17 | 18 | 19 | ## In-depth Description 20 | 21 | 26 | 27 | ## How have you tested this? 28 | 29 | 30 | 31 | ## Screenshots 32 | 33 | 34 | -------------------------------------------------------------------------------- /.github/workflows/close-issues-on-release.yml: -------------------------------------------------------------------------------- 1 | name: Close fixed issues on release. 2 | on: 3 | release: 4 | types: [published] 5 | 6 | permissions: 7 | contents: read 8 | issues: write 9 | 10 | jobs: 11 | comment: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Close issues marked as fixed upon a release. 15 | uses: gcampbell-msft/fixed-pending-release@7fa1b75a0c04bcd4b375110522878e5f6100cff5 16 | with: 17 | label: 'awaiting release' 18 | removeLabel: true 19 | applyToAll: true 20 | message: Fixed in [${releaseTag}](${releaseUrl}). 21 | -------------------------------------------------------------------------------- /.github/workflows/component-tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Component Tests 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | ref: 7 | description: 'Branch/Tag/SHA to test' 8 | required: true 9 | pull_request: 10 | paths: 11 | - 'client/**' 12 | - '.github/workflows/component-tests.yml' 13 | push: 14 | paths: 15 | - 'client/**' 16 | - '.github/workflows/component-tests.yml' 17 | 18 | jobs: 19 | run-component-tests: 20 | name: Run Component Tests 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - name: Checkout (push/pull request) 25 | uses: actions/checkout@v4 26 | if: github.event_name != 'workflow_dispatch' 27 | 28 | - name: Checkout (workflow_dispatch) 29 | uses: actions/checkout@v4 30 | with: 31 | ref: ${{ inputs.ref }} 32 | if: github.event_name == 'workflow_dispatch' 33 | 34 | - name: Set up Node.js 35 | uses: actions/setup-node@v4 36 | with: 37 | node-version: 20 38 | cache: 'npm' 39 | 40 | - name: Install dependencies 41 | run: | 42 | cd client 43 | npm ci 44 | 45 | - name: Run tests 46 | run: | 47 | cd client 48 | npm test 49 | -------------------------------------------------------------------------------- /.github/workflows/i18n-integration.yml: -------------------------------------------------------------------------------- 1 | name: Verify all i18n files are alphabetized 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - client/strings/** # Should only check if any strings changed 7 | push: 8 | paths: 9 | - client/strings/** # Should only check if any strings changed 10 | 11 | jobs: 12 | update_translations: 13 | runs-on: ubuntu-latest 14 | steps: 15 | # Check out the repository 16 | - name: Checkout repository 17 | uses: actions/checkout@v4 18 | 19 | # Set up node to run the javascript 20 | - name: Set up node 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: 20 24 | cache: 'npm' 25 | 26 | # The only argument is the `directory`, which is where the i18n files are 27 | # stored. 28 | - name: Run Update JSON Files action 29 | uses: audiobookshelf/audiobookshelf-i18n-updater@v1.3.0 30 | with: 31 | directory: 'client/strings/' # Adjust the directory path as needed 32 | -------------------------------------------------------------------------------- /.github/workflows/integration-test.yml: -------------------------------------------------------------------------------- 1 | name: Integration Test 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches-ignore: 7 | - 'dependabot/**' # Don't run dependabot branches, as they are already covered by pull requests 8 | # Only build when files in these directories have been changed 9 | paths: 10 | - client/** 11 | - server/** 12 | - test/** 13 | - index.js 14 | - package.json 15 | 16 | jobs: 17 | build: 18 | name: build and test 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - name: setup node 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: 20 27 | cache: 'npm' 28 | 29 | - name: install pkg (using yao-pkg fork for targeting node20) 30 | run: npm install -g @yao-pkg/pkg 31 | 32 | - name: get client dependencies 33 | working-directory: client 34 | run: npm ci 35 | 36 | - name: build client 37 | working-directory: client 38 | run: npm run generate 39 | 40 | - name: get server dependencies 41 | run: npm ci --only=production 42 | 43 | - name: build binary 44 | run: pkg -t node20-linux-x64 -o audiobookshelf . 45 | 46 | - name: run audiobookshelf 47 | run: | 48 | ./audiobookshelf & 49 | sleep 5 50 | 51 | - name: test if server is available 52 | run: curl -sf http://127.0.0.1:3333 | grep Audiobookshelf 53 | -------------------------------------------------------------------------------- /.github/workflows/lint-openapi.yml: -------------------------------------------------------------------------------- 1 | name: API linting 2 | 3 | # Run on pull requests or pushes when there is a change to any OpenAPI files in docs/ 4 | on: 5 | pull_request: 6 | push: 7 | paths: 8 | - 'docs/**' 9 | 10 | # This action only needs read permissions 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | steps: 18 | # Check out the repository 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | 22 | # Set up node to run the javascript 23 | - name: Set up node 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: 20 27 | cache: 'npm' 28 | 29 | # Install Redocly CLI 30 | - name: Install Redocly CLI 31 | run: npm install -g @redocly/cli@latest 32 | 33 | # Perform linting for exploded spec 34 | - name: Run linting for exploded spec 35 | run: redocly lint docs/root.yaml --format=github-actions 36 | 37 | # Perform linting for bundled spec 38 | - name: Run linting for bundled spec 39 | run: redocly lint docs/openapi.json --format=github-actions 40 | -------------------------------------------------------------------------------- /.github/workflows/notify-abs-windows.yml: -------------------------------------------------------------------------------- 1 | name: Dispatch an abs-windows event 2 | 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | abs-windows-dispatch: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Send a remote repository dispatch event 13 | uses: peter-evans/repository-dispatch@v3 14 | with: 15 | token: ${{ secrets.ABS_WINDOWS_PAT }} 16 | repository: mikiher/audiobookshelf-windows 17 | event-type: build-windows 18 | -------------------------------------------------------------------------------- /.github/workflows/unit-tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Unit Tests 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | ref: 7 | description: 'Branch/Tag/SHA to test' 8 | required: true 9 | pull_request: 10 | push: 11 | 12 | jobs: 13 | run-unit-tests: 14 | name: Run Unit Tests 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout (push/pull request) 19 | uses: actions/checkout@v4 20 | if: github.event_name != 'workflow_dispatch' 21 | 22 | - name: Checkout (workflow_dispatch) 23 | uses: actions/checkout@v4 24 | with: 25 | ref: ${{ inputs.ref }} 26 | if: github.event_name == 'workflow_dispatch' 27 | 28 | - name: Set up Node.js 29 | uses: actions/setup-node@v4 30 | with: 31 | node-version: 20 32 | cache: 'npm' 33 | 34 | - name: Install dependencies 35 | run: npm ci 36 | 37 | - name: Run tests 38 | run: npm test 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | /dev.js 3 | **/node_modules/ 4 | /config/ 5 | /audiobooks/ 6 | /audiobooks2/ 7 | /podcasts/ 8 | /media/ 9 | /metadata/ 10 | /plugins/ 11 | /client/.nuxt/ 12 | /client/dist/ 13 | /dist/ 14 | /deploy/ 15 | /coverage/ 16 | /.nyc_output/ 17 | /ffmpeg* 18 | /ffprobe* 19 | /unicode* 20 | /libnusqlite3* 21 | 22 | sw.* 23 | .DS_STORE 24 | .idea/* 25 | tailwind.compiled.css 26 | tailwind.config.js 27 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "printWidth": 400, 5 | "proseWrap": "never", 6 | "trailingComma": "none", 7 | "overrides": [ 8 | { 9 | "files": ["*.html"], 10 | "options": { 11 | "singleQuote": false, 12 | "wrapAttributes": false, 13 | "sortAttributes": false 14 | } 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "EditorConfig.EditorConfig", 4 | "esbenp.prettier-vscode", 5 | "octref.vetur" 6 | ] 7 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Debug server", 11 | "runtimeExecutable": "npm", 12 | "args": [ 13 | "run", 14 | "dev" 15 | ], 16 | "skipFiles": [ 17 | "/**" 18 | ] 19 | }, 20 | { 21 | "type": "node", 22 | "request": "launch", 23 | "name": "Debug client (nuxt)", 24 | "runtimeExecutable": "npm", 25 | "args": [ 26 | "run", 27 | "dev" 28 | ], 29 | "cwd": "${workspaceFolder}/client", 30 | "skipFiles": [ 31 | "${workspaceFolder}//**" 32 | ] 33 | } 34 | ], 35 | "compounds": [ 36 | { 37 | "name": "Debug server and client (nuxt)", 38 | "configurations": [ 39 | "Debug server", 40 | "Debug client (nuxt)" 41 | ] 42 | } 43 | ] 44 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "vetur.format.defaultFormatterOptions": { 3 | "prettier": { 4 | "semi": false, 5 | "singleQuote": true, 6 | "printWidth": 400, 7 | "proseWrap": "never", 8 | "trailingComma": "none" 9 | }, 10 | "prettyhtml": { 11 | "printWidth": 400, 12 | "singleQuote": false, 13 | "wrapAttributes": false, 14 | "sortAttributes": false 15 | } 16 | }, 17 | "editor.formatOnSave": true, 18 | "editor.detectIndentation": true, 19 | "editor.tabSize": 2, 20 | "javascript.format.semicolons": "remove", 21 | "[javascript][json][jsonc]": { 22 | "editor.defaultFormatter": "esbenp.prettier-vscode" 23 | }, 24 | "[vue]": { 25 | "editor.defaultFormatter": "octref.vetur" 26 | } 27 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "path": "client", 6 | "type": "npm", 7 | "script": "generate", 8 | "detail": "nuxt generate", 9 | "label": "Build client", 10 | "group": { 11 | "kind": "build", 12 | "isDefault": true 13 | } 14 | }, 15 | { 16 | "dependsOn": [ 17 | "Build client" 18 | ], 19 | "type": "npm", 20 | "script": "dev", 21 | "detail": "nodemon --watch server index.js", 22 | "label": "Run server", 23 | "group": { 24 | "kind": "test", 25 | "isDefault": true 26 | } 27 | }, 28 | { 29 | "path": "client", 30 | "type": "npm", 31 | "script": "dev", 32 | "detail": "nuxt", 33 | "label": "Run Live-reload client", 34 | "group": { 35 | "kind": "test", 36 | "isDefault": false 37 | } 38 | } 39 | ] 40 | } -------------------------------------------------------------------------------- /build/debian/DEBIAN/control: -------------------------------------------------------------------------------- 1 | Package: audiobookshelf 2 | Version: 1.6.41 3 | Section: base 4 | Priority: optional 5 | Architecture: amd64 6 | Depends: 7 | Maintainer: advplyr 8 | Description: Self-hosted audiobook server for managing and playing audiobooks 9 | -------------------------------------------------------------------------------- /build/debian/DEBIAN/prerm: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | set -o pipefail 4 | 5 | declare -r init_type='auto' 6 | declare -r service_name='audiobookshelf' 7 | 8 | if [[ "$init_type" == 'auto' || "$init_type" == 'systemd' || "$init_type" == 'upstart' || "$init_type" == 'sysv' ]]; then 9 | if hash systemctl 2> /dev/null; then 10 | systemctl disable "$service_name.service" && \ 11 | systemctl stop "$service_name.service" || \ 12 | echo "$service_name wasn't even running!" 13 | elif hash service 2> /dev/null; then 14 | service "$service_name" stop || echo "$service_name wasn't even running!" 15 | elif hash stop 2> /dev/null; then 16 | stop "$service_name" || echo "$service_name wasn't even running!" 17 | elif hash update-rc.d 2> /dev/null; then 18 | { 19 | update-rc.d "$service_name" remove && \ 20 | "/etc/init.d/$service_name" stop 21 | } || "$service_name wasn't even running!" 22 | else 23 | echo "Your system does not appear to use upstart, systemd or sysv, so $service_name could not be stopped" 24 | echo 'Unless these systems were removed since install, no processes have been left running' 25 | fi 26 | fi -------------------------------------------------------------------------------- /build/debian/etc/default/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/advplyr/audiobookshelf/c377b57601f82f76d677b09e6bbabda732c18861/build/debian/etc/default/.gitkeep -------------------------------------------------------------------------------- /build/debian/lib/systemd/system/audiobookshelf.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Self-hosted audiobook server for managing and playing audiobooks 3 | Requires=network.target 4 | 5 | [Service] 6 | Type=simple 7 | EnvironmentFile=/etc/default/audiobookshelf 8 | WorkingDirectory=/usr/share/audiobookshelf 9 | ExecStart=/usr/share/audiobookshelf/audiobookshelf 10 | ExecReload=/bin/kill -HUP $MAINPID 11 | Restart=always 12 | User=audiobookshelf 13 | Group=audiobookshelf 14 | 15 | [Install] 16 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /build/debian/usr/lib/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/advplyr/audiobookshelf/c377b57601f82f76d677b09e6bbabda732c18861/build/debian/usr/lib/.gitkeep -------------------------------------------------------------------------------- /build/debian/usr/share/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/advplyr/audiobookshelf/c377b57601f82f76d677b09e6bbabda732c18861/build/debian/usr/share/.gitkeep -------------------------------------------------------------------------------- /build/linuxpackager: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | set -o pipefail 4 | 5 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 6 | 7 | cd "$SCRIPT_DIR/.." 8 | 9 | # Get package version without double quotes 10 | VERSION="$( eval echo $( jq '.version' package.json) )" 11 | DESCRIPTION="$( eval echo $( jq '.description' package.json) )" 12 | OUTPUT_FILE="audiobookshelf_${VERSION}_amd64.deb" 13 | 14 | echo ">>> Building Client" 15 | echo "--------------------" 16 | 17 | cd client 18 | rm -rf node_modules 19 | npm ci --unsafe-perm=true --allow-root 20 | npm run generate 21 | cd .. 22 | 23 | echo ">>> Building Server" 24 | echo "--------------------" 25 | 26 | rm -rf node_modules 27 | npm ci --unsafe-perm=true --allow-root 28 | 29 | echo ">>> Packaging" 30 | echo "--------------------" 31 | 32 | # Create debian control file 33 | 34 | mkdir -p dist 35 | rm -rf dist/debian 36 | cp -R build/debian dist/debian 37 | chmod -R 775 dist/debian 38 | 39 | controlfile="Package: audiobookshelf 40 | Version: $VERSION 41 | Section: base 42 | Priority: optional 43 | Architecture: amd64 44 | Depends: 45 | Maintainer: advplyr 46 | Description: $DESCRIPTION" 47 | 48 | echo "$controlfile" > dist/debian/DEBIAN/control; 49 | 50 | # Package debian 51 | pkg -t node20-linux-x64 -o dist/debian/usr/share/audiobookshelf/audiobookshelf . 52 | 53 | fakeroot dpkg-deb -Zxz --build dist/debian 54 | 55 | mv dist/debian.deb "dist/$OUTPUT_FILE" 56 | 57 | echo "Finished! Filename: $OUTPUT_FILE" 58 | -------------------------------------------------------------------------------- /client/assets/draggable.css: -------------------------------------------------------------------------------- 1 | .flip-list-move { 2 | transition: transform 0.5s; 3 | } 4 | 5 | .no-move { 6 | transition: transform 0s; 7 | } 8 | 9 | .ghost { 10 | opacity: 0.5; 11 | background-color: rgba(255, 255, 255, 0.25); 12 | } 13 | 14 | .list-group { 15 | min-height: 30px; 16 | } 17 | 18 | .drag-handle { 19 | cursor: n-resize; 20 | } 21 | 22 | .list-group-item:not(.exclude) { 23 | cursor: n-resize; 24 | } 25 | 26 | .list-group-item.exclude { 27 | cursor: not-allowed; 28 | } 29 | 30 | .list-group-item:not(.ghost):not(.exclude):hover { 31 | background-color: rgba(0, 0, 0, 0.1); 32 | } 33 | 34 | .list-group-item:nth-child(even):not(.ghost):not(.exclude) { 35 | background-color: rgba(0, 0, 0, 0.25); 36 | } 37 | 38 | .list-group-item:nth-child(even):not(.ghost):not(.exclude):hover { 39 | background-color: rgba(0, 0, 0, 0.1); 40 | } 41 | 42 | .list-group-item.exclude:not(.ghost) { 43 | background-color: rgba(255, 0, 0, 0.25); 44 | } 45 | 46 | .list-group-item.exclude:not(.ghost):hover { 47 | background-color: rgba(223, 0, 0, 0.25); 48 | } -------------------------------------------------------------------------------- /client/components/app/SettingsContent.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 26 | 27 | 43 | -------------------------------------------------------------------------------- /client/components/cards/AuthorSearchCard.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 36 | 37 | 46 | -------------------------------------------------------------------------------- /client/components/cards/GenreSearchCard.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 27 | 28 | 37 | -------------------------------------------------------------------------------- /client/components/cards/NarratorSearchCard.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 27 | 28 | 37 | -------------------------------------------------------------------------------- /client/components/cards/SeriesSearchCard.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 37 | 38 | -------------------------------------------------------------------------------- /client/components/cards/TagSearchCard.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 27 | 28 | 37 | -------------------------------------------------------------------------------- /client/components/modals/RawCoverPreviewModal.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 31 | -------------------------------------------------------------------------------- /client/components/modals/item/tabs/Chapters.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 47 | -------------------------------------------------------------------------------- /client/components/modals/item/tabs/Files.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /client/components/ui/EditableText.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 50 | 51 | 59 | -------------------------------------------------------------------------------- /client/components/ui/FileInput.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 44 | -------------------------------------------------------------------------------- /client/components/ui/LibraryIcon.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /client/components/ui/RichTextEditor.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 47 | -------------------------------------------------------------------------------- /client/components/ui/TextareaInput.vue: -------------------------------------------------------------------------------- 1 |