diff --git a/client/pages/config/index.vue b/client/pages/config/index.vue
index 7f0170c7..93a4f803 100644
--- a/client/pages/config/index.vue
+++ b/client/pages/config/index.vue
@@ -39,11 +39,15 @@
             <ui-multi-select v-model="newServerSettings.sortingPrefixes" small :items="newServerSettings.sortingPrefixes" :label="$strings.LabelPrefixesToIgnore" @input="updateSortingPrefixes" :disabled="updatingServerSettings" />
           </div>
 
-          <div class="flex items-center py-2">
+          <div class="flex items-center py-2 mb-2">
             <ui-toggle-switch labeledBy="settings-chromecast-support" v-model="newServerSettings.chromecastEnabled" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('chromecastEnabled', val)" />
             <p class="pl-4" id="settings-chromecast-support">{{ $strings.LabelSettingsChromecastSupport }}</p>
           </div>
 
+          <div class="w-44 mb-2">
+            <ui-dropdown v-model="newServerSettings.metadataFileFormat" small :items="metadataFileFormats" label="Metadata File Format" @input="(val) => updateSettingsKey('metadataFileFormat', val)" :disabled="updatingServerSettings" />
+          </div>
+
           <div class="pt-4">
             <h2 class="font-semibold">{{ $strings.HeaderSettingsDisplay }}</h2>
           </div>
@@ -272,7 +276,17 @@ export default {
       useBookshelfView: false,
       isPurgingCache: false,
       newServerSettings: {},
-      showConfirmPurgeCache: false
+      showConfirmPurgeCache: false,
+      metadataFileFormats: [
+        {
+          text: '.json',
+          value: 'json'
+        },
+        {
+          text: '.abs',
+          value: 'abs'
+        }
+      ]
     }
   },
   watch: {
diff --git a/server/objects/LibraryItem.js b/server/objects/LibraryItem.js
index b15e53c9..0d001799 100644
--- a/server/objects/LibraryItem.js
+++ b/server/objects/LibraryItem.js
@@ -499,14 +499,37 @@ class LibraryItem {
       // Make sure metadata book dir exists
       await fs.ensureDir(metadataPath)
     }
-    metadataPath = Path.join(metadataPath, 'metadata.abs')
 
-    return abmetadataGenerator.generate(this, metadataPath).then((success) => {
-      this.isSavingMetadata = false
-      if (!success) Logger.error(`[LibraryItem] Failed saving abmetadata to "${metadataPath}"`)
-      else Logger.debug(`[LibraryItem] Success saving abmetadata to "${metadataPath}"`)
-      return success
-    })
+    const metadataFileFormat = global.ServerSettings.metadataFileFormat
+    const metadataFilePath = Path.join(metadataPath, `metadata.${metadataFileFormat}`)
+
+    if (metadataFileFormat === 'json') {
+      // Remove metadata.abs if it exists
+      if (await fs.pathExists(Path.join(metadataPath, `metadata.abs`))) {
+        Logger.debug(`[LibraryItem] Removing metadata.abs for item "${this.media.metadata.title}"`)
+        await fs.remove(Path.join(metadataPath, `metadata.abs`))
+      }
+
+      return fs.writeFile(metadataFilePath, JSON.stringify(this.media.toJSONForMetadataFile(), null, 2)).then(() => {
+        return true
+      }).catch((error) => {
+        Logger.error(`[LibraryItem] Failed to save json file at "${metadataFilePath}"`, error)
+        return false
+      })
+    } else {
+      // Remove metadata.json if it exists
+      if (await fs.pathExists(Path.join(metadataPath, `metadata.json`))) {
+        Logger.debug(`[LibraryItem] Removing metadata.json for item "${this.media.metadata.title}"`)
+        await fs.remove(Path.join(metadataPath, `metadata.json`))
+      }
+
+      return abmetadataGenerator.generate(this, metadataFilePath).then((success) => {
+        this.isSavingMetadata = false
+        if (!success) Logger.error(`[LibraryItem] Failed saving abmetadata to "${metadataFilePath}"`)
+        else Logger.debug(`[LibraryItem] Success saving abmetadata to "${metadataFilePath}"`)
+        return success
+      })
+    }
   }
 
   removeLibraryFile(ino) {
diff --git a/server/objects/mediaTypes/Book.js b/server/objects/mediaTypes/Book.js
index 18c6ea52..d0cfab93 100644
--- a/server/objects/mediaTypes/Book.js
+++ b/server/objects/mediaTypes/Book.js
@@ -89,6 +89,14 @@ class Book {
     }
   }
 
+  toJSONForMetadataFile() {
+    return {
+      tags: [...this.tags],
+      chapters: this.chapters.map(c => ({ ...c })),
+      metadata: this.metadata.toJSONForMetadataFile()
+    }
+  }
+
   get size() {
     var total = 0
     this.audioFiles.forEach((af) => total += af.metadata.size)
@@ -248,11 +256,12 @@ class Book {
       }
     }
 
-    const metadataAbs = textMetadataFiles.find(lf => lf.metadata.filename === 'metadata.abs')
+    const metadataAbs = textMetadataFiles.find(lf => lf.metadata.filename === 'metadata.abs' || lf.metadata.filename === 'metadata.json')
     if (metadataAbs) {
-      Logger.debug(`[Book] Found metadata.abs file for "${this.metadata.title}"`)
+      const isJSON = metadataAbs.metadata.filename === 'metadata.json'
+      Logger.debug(`[Book] Found ${metadataAbs.metadata.filename} file for "${this.metadata.title}"`)
       const metadataText = await readTextFile(metadataAbs.metadata.path)
-      const abmetadataUpdates = abmetadataGenerator.parseAndCheckForUpdates(metadataText, this, 'book')
+      const abmetadataUpdates = abmetadataGenerator.parseAndCheckForUpdates(metadataText, this, 'book', isJSON)
       if (abmetadataUpdates && Object.keys(abmetadataUpdates).length) {
         Logger.debug(`[Book] "${this.metadata.title}" changes found in metadata.abs file`, abmetadataUpdates)
 
diff --git a/server/objects/mediaTypes/Podcast.js b/server/objects/mediaTypes/Podcast.js
index 46c34a39..e1227deb 100644
--- a/server/objects/mediaTypes/Podcast.js
+++ b/server/objects/mediaTypes/Podcast.js
@@ -94,6 +94,13 @@ class Podcast {
     }
   }
 
+  toJSONForMetadataFile() {
+    return {
+      tags: [...this.tags],
+      metadata: this.metadata.toJSON()
+    }
+  }
+
   get size() {
     var total = 0
     this.episodes.forEach((ep) => total += ep.size)
@@ -199,10 +206,11 @@ class Podcast {
     let metadataUpdatePayload = {}
     let tagsUpdated = false
 
-    const metadataAbs = textMetadataFiles.find(lf => lf.metadata.filename === 'metadata.abs')
+    const metadataAbs = textMetadataFiles.find(lf => lf.metadata.filename === 'metadata.abs' || lf.metadata.filename === 'metadata.json')
     if (metadataAbs) {
+      const isJSON = metadataAbs.metadata.filename === 'metadata.json'
       const metadataText = await readTextFile(metadataAbs.metadata.path)
-      const abmetadataUpdates = abmetadataGenerator.parseAndCheckForUpdates(metadataText, this, 'podcast')
+      const abmetadataUpdates = abmetadataGenerator.parseAndCheckForUpdates(metadataText, this, 'podcast', isJSON)
       if (abmetadataUpdates && Object.keys(abmetadataUpdates).length) {
         Logger.debug(`[Podcast] "${this.metadata.title}" changes found in metadata.abs file`, abmetadataUpdates)
 
diff --git a/server/objects/metadata/BookMetadata.js b/server/objects/metadata/BookMetadata.js
index 586d699a..7cdbdcff 100644
--- a/server/objects/metadata/BookMetadata.js
+++ b/server/objects/metadata/BookMetadata.js
@@ -109,6 +109,16 @@ class BookMetadata {
     }
   }
 
+  toJSONForMetadataFile() {
+    const json = this.toJSON()
+    json.authors = json.authors.map(au => au.name)
+    json.series = json.series.map(se => {
+      if (!se.sequence) return se.name
+      return `${se.name} #${se.sequence}`
+    })
+    return json
+  }
+
   clone() {
     return new BookMetadata(this.toJSON())
   }
@@ -191,8 +201,9 @@ class BookMetadata {
   }
 
   update(payload) {
-    var json = this.toJSON()
-    var hasUpdates = false
+    const json = this.toJSON()
+    let hasUpdates = false
+
     for (const key in json) {
       if (payload[key] !== undefined) {
         if (!areEquivalent(payload[key], json[key])) {
@@ -373,8 +384,10 @@ class BookMetadata {
     const parsed = parseNameString.parse(authorsTag)
     if (!parsed) return []
     return (parsed.names || []).map((au) => {
+      const findAuthor = this.authors.find(_au => _au.name == au)
+
       return {
-        id: `new-${Math.floor(Math.random() * 1000000)}`,
+        id: findAuthor?.id || `new-${Math.floor(Math.random() * 1000000)}`,
         name: au
       }
     })
diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js
index 1dbd3417..16855c38 100644
--- a/server/objects/settings/ServerSettings.js
+++ b/server/objects/settings/ServerSettings.js
@@ -1,5 +1,4 @@
 const { BookshelfView } = require('../../utils/constants')
-const { isNullOrNaN } = require('../../utils')
 const Logger = require('../../Logger')
 
 class ServerSettings {
@@ -21,6 +20,7 @@ class ServerSettings {
     // Metadata - choose to store inside users library item folder
     this.storeCoverWithItem = false
     this.storeMetadataWithItem = false
+    this.metadataFileFormat = 'json'
 
     // Security/Rate limits
     this.rateLimitLoginRequests = 10
@@ -77,6 +77,7 @@ class ServerSettings {
 
     this.storeCoverWithItem = !!settings.storeCoverWithItem
     this.storeMetadataWithItem = !!settings.storeMetadataWithItem
+    this.metadataFileFormat = settings.metadataFileFormat || 'json'
 
     this.rateLimitLoginRequests = !isNaN(settings.rateLimitLoginRequests) ? Number(settings.rateLimitLoginRequests) : 10
     this.rateLimitLoginWindow = !isNaN(settings.rateLimitLoginWindow) ? Number(settings.rateLimitLoginWindow) : 10 * 60 * 1000 // 10 Minutes
@@ -112,6 +113,16 @@ class ServerSettings {
     if (settings.homeBookshelfView == undefined) { // homeBookshelfView was added in 2.1.3
       this.homeBookshelfView = settings.bookshelfView
     }
+    if (settings.metadataFileFormat == undefined) { // metadataFileFormat was added in 2.2.21
+      // All users using old settings will stay abs until changed
+      this.metadataFileFormat = 'abs'
+    }
+
+    // Validation
+    if (!['abs', 'json'].includes(this.metadataFileFormat)) {
+      Logger.error(`[ServerSettings] construct: Invalid metadataFileFormat ${this.metadataFileFormat}`)
+      this.metadataFileFormat = 'json'
+    }
 
     if (this.logLevel !== Logger.logLevel) {
       Logger.setLogLevel(this.logLevel)
@@ -133,6 +144,7 @@ class ServerSettings {
       scannerUseTone: this.scannerUseTone,
       storeCoverWithItem: this.storeCoverWithItem,
       storeMetadataWithItem: this.storeMetadataWithItem,
+      metadataFileFormat: this.metadataFileFormat,
       rateLimitLoginRequests: this.rateLimitLoginRequests,
       rateLimitLoginWindow: this.rateLimitLoginWindow,
       backupSchedule: this.backupSchedule,
diff --git a/server/utils/abmetadataGenerator.js b/server/utils/abmetadataGenerator.js
index e6fb0b70..26cffbae 100644
--- a/server/utils/abmetadataGenerator.js
+++ b/server/utils/abmetadataGenerator.js
@@ -2,7 +2,7 @@ const fs = require('../libs/fsExtra')
 const filePerms = require('./filePerms')
 const package = require('../../package.json')
 const Logger = require('../Logger')
-const { getId, copyValue } = require('./index')
+const { getId } = require('./index')
 
 
 const CurrentAbMetadataVersion = 2
@@ -328,11 +328,11 @@ function parseAbMetadataText(text, mediaType) {
 module.exports.parse = parseAbMetadataText
 
 function checkUpdatedBookAuthors(abmetadataAuthors, authors) {
-  var finalAuthors = []
-  var hasUpdates = false
+  const finalAuthors = []
+  let hasUpdates = false
 
   abmetadataAuthors.forEach((authorName) => {
-    var findAuthor = authors.find(au => au.name.toLowerCase() == authorName.toLowerCase())
+    const findAuthor = authors.find(au => au.name.toLowerCase() == authorName.toLowerCase())
     if (!findAuthor) {
       hasUpdates = true
       finalAuthors.push({
@@ -397,18 +397,54 @@ function checkArraysChanged(abmetadataArray, mediaArray) {
   return abmetadataArray.join(',') != mediaArray.join(',')
 }
 
+function parseJsonMetadataText(text) {
+  try {
+    const abmetadataData = JSON.parse(text)
+    if (abmetadataData.metadata?.series?.length) {
+      abmetadataData.metadata.series = abmetadataData.metadata.series.map(series => {
+        let sequence = null
+        let name = series
+        // Series sequence match any characters after " #" other than whitespace and another #
+        //  e.g. "Name #1a" is valid. "Name #1#a" or "Name #1 a" is not valid.
+        const matchResults = series.match(/ #([^#\s]+)$/) // Pull out sequence #
+        if (matchResults && matchResults.length && matchResults.length > 1) {
+          sequence = matchResults[1] // Group 1
+          name = series.replace(matchResults[0], '')
+        }
+        return {
+          name,
+          sequence
+        }
+      })
+    }
+    return abmetadataData
+  } catch (error) {
+    Logger.error(`[abmetadataGenerator] Invalid metadata.json JSON`, error)
+    return null
+  }
+}
+
 // Input text from abmetadata file and return object of media changes
 //  only returns object of changes. empty object means no changes
-function parseAndCheckForUpdates(text, media, mediaType) {
+function parseAndCheckForUpdates(text, media, mediaType, isJSON) {
   if (!text || !media || !media.metadata || !mediaType) {
     Logger.error(`Invalid inputs to parseAndCheckForUpdates`)
     return null
   }
+
   const mediaMetadata = media.metadata
   const metadataUpdatePayload = {} // Only updated key/values
 
-  const abmetadataData = parseAbMetadataText(text, mediaType)
+  let abmetadataData = null
+
+  if (isJSON) {
+    abmetadataData = parseJsonMetadataText(text)
+  } else {
+    abmetadataData = parseAbMetadataText(text, mediaType)
+  }
+
   if (!abmetadataData || !abmetadataData.metadata) {
+    Logger.error(`[abmetadataGenerator] Invalid metadata file`)
     return null
   }
 
diff --git a/server/utils/globals.js b/server/utils/globals.js
index 71c1c2c0..7d20dfe3 100644
--- a/server/utils/globals.js
+++ b/server/utils/globals.js
@@ -4,7 +4,7 @@ const globals = {
   SupportedEbookTypes: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'],
   SupportedVideoTypes: ['mp4'],
   TextFileTypes: ['txt', 'nfo'],
-  MetadataFileTypes: ['opf', 'abs', 'xml']
+  MetadataFileTypes: ['opf', 'abs', 'xml', 'json']
 }
 
 module.exports = globals