Add chapter title scraping and improve error logging

This commit is contained in:
Harry Rose 2026-03-10 20:43:57 +00:00
parent e8d65ceb88
commit b4b126e39f
2 changed files with 61 additions and 45 deletions

View file

@ -1,40 +1,32 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the // For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/javascript-node // README at: https://github.com/devcontainers/templates/tree/main/src/javascript-node
{ {
"name": "Audiobookshelf", "name": "Audiobookshelf",
"build": { "build": {
"dockerfile": "Dockerfile", "dockerfile": "Dockerfile",
// Update 'VARIANT' to pick a Node version: 18, 16, 14. // Update 'VARIANT' to pick a Node version: 18, 16, 14.
// Append -bullseye or -buster to pin to an OS version. // Append -bullseye or -buster to pin to an OS version.
// Use -bullseye variants on local arm64/Apple Silicon. // Use -bullseye variants on local arm64/Apple Silicon.
"args": { "args": {
"VARIANT": "20" "VARIANT": "20"
} }
}, },
"mounts": [ "mounts": ["source=abs-server-node_modules,target=${containerWorkspaceFolder}/node_modules,type=volume", "source=abs-client-node_modules,target=${containerWorkspaceFolder}/client/node_modules,type=volume", "source=/home/harry/Music/ABS-Dev,target=/podcasts,type=bind,consistency=cached"],
"source=abs-server-node_modules,target=${containerWorkspaceFolder}/node_modules,type=volume", // Features to add to the dev container. More info: https://containers.dev/features.
"source=abs-client-node_modules,target=${containerWorkspaceFolder}/client/node_modules,type=volume" // "features": {},
], // Use 'forwardPorts' to make a list of ports inside the container available locally.
// Features to add to the dev container. More info: https://containers.dev/features. "forwardPorts": [3000, 3333],
// "features": {}, // Use 'postCreateCommand' to run commands after the container is created.
// Use 'forwardPorts' to make a list of ports inside the container available locally. "postCreateCommand": "sh .devcontainer/post-create.sh",
"forwardPorts": [ // Configure tool-specific properties.
3000, "customizations": {
3333 // Configure properties specific to VS Code.
], "vscode": {
// Use 'postCreateCommand' to run commands after the container is created. // Add the IDs of extensions you want installed when the container is created.
"postCreateCommand": "sh .devcontainer/post-create.sh", "extensions": ["dbaeumer.vscode-eslint", "octref.vetur"]
// Configure tool-specific properties. }
"customizations": { },
// Configure properties specific to VS Code. "runArgs": ["-p=3333:3333"]
"vscode": { // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// Add the IDs of extensions you want installed when the container is created. // "remoteUser": "root"
"extensions": [ }
"dbaeumer.vscode-eslint",
"octref.vetur"
]
}
}
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}

View file

@ -1,6 +1,7 @@
const { DataTypes, Model } = require('sequelize') const { DataTypes, Model } = require('sequelize')
const libraryItemsPodcastFilters = require('../utils/queries/libraryItemsPodcastFilters') const libraryItemsPodcastFilters = require('../utils/queries/libraryItemsPodcastFilters')
const Logger = require('../Logger') const Logger = require('../Logger')
const { logger } = require('sequelize/lib/utils/logger')
/** /**
* @typedef ChapterObject * @typedef ChapterObject
* @property {number} id * @property {number} id
@ -87,11 +88,15 @@ class PodcastEpisode extends Model {
} else if (rssPodcastEpisode.chapters?.length) { } else if (rssPodcastEpisode.chapters?.length) {
podcastEpisode.chapters = rssPodcastEpisode.chapters.map((ch) => ({ ...ch })) podcastEpisode.chapters = rssPodcastEpisode.chapters.map((ch) => ({ ...ch }))
} else { } else {
const timeMarkerRegex = /\b(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?\b/m const timeMarkerRegex = /\b(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?\b/
const chapterTitleRegex = /\b\d{1,2}:\d{1,2}(?::\d{1,2})?\b(.+)$/
Logger.debug("Podcast didn't have chapters", rssPodcastEpisode.title) Logger.debug("Podcast episode doesn't have chapters, attempting to generate them from timestamps", rssPodcastEpisode.title)
var errorMessage = null
var descriptionLines = podcastEpisode.description.split('</p>') var descriptionLines = podcastEpisode.description.split('</p>')
var chaptersToPush = []
for (let i = 0; i < descriptionLines.length; i++) { for (let i = 0; i < descriptionLines.length; i++) {
let line = descriptionLines[i] let line = descriptionLines[i]
Logger.debug('Description Line:', line) Logger.debug('Description Line:', line)
@ -99,7 +104,7 @@ class PodcastEpisode extends Model {
let match = timeMarkerRegex.exec(line) let match = timeMarkerRegex.exec(line)
if (match == null) continue if (match == null) continue
Logger.debug('matches:', match) Logger.debug('Matches:', match)
let first = match[1] let first = match[1]
let second = match[2] let second = match[2]
@ -118,23 +123,42 @@ class PodcastEpisode extends Model {
{ {
minutes = Number(first) minutes = Number(first)
seconds = Number(second) seconds = Number(second)
} else {
// Unknown timestamp state
errorMessage = `Unknown timestamp format in description, line ${line}`
break
} }
let startTime = seconds + minutes * 60 + hours * 60 * 60 let startTime = seconds + minutes * 60 + hours * 60 * 60
let chapter = { title: `Chapter ${i}`, id: i, start: startTime } let chapterTitleMatch = chapterTitleRegex.exec(line)
Logger.debug('Chapter Title Matches:', chapterTitleMatch)
if (chapterTitleMatch == null && chapterTitleMatch.length >= 2) {
// Unknown chapter state
errorMessage = `Unable to get chapter title from description, line ${line}`
break
}
let chapter = { title: chapterTitleMatch[1].trim(), id: i, start: startTime }
if (podcastEpisode.chapters.length > 0) { if (podcastEpisode.chapters.length > 0) {
podcastEpisode.chapters[podcastEpisode.chapters.length - 1].end = startTime podcastEpisode.chapters[podcastEpisode.chapters.length - 1].end = startTime
} }
podcastEpisode.chapters.push(chapter) chaptersToPush.push(chapter)
Logger.debug('Added chapter', chapter) Logger.debug('Added chapter', chapter)
} }
if (podcastEpisode.chapters.length > 0) { if (errorMessage == null) {
podcastEpisode.chapters[podcastEpisode.chapters.length - 1].end = podcastEpisode.audioFile.duration if (podcastEpisode.chapters.length > 0) {
podcastEpisode.chapters[podcastEpisode.chapters.length - 1].end = podcastEpisode.audioFile.duration
}
podcastEpisode.chapters.push(...chaptersToPush)
Logger.debug(`Successfully gnerated ${podcastEpisode.chapters.length} chapters`)
} else {
logger.error(`Unable generate chapters from podcast description, error '${errorMessage}`)
} }
Logger.debug('Chapters', podcastEpisode.chapters)
} }
return this.create(podcastEpisode) return this.create(podcastEpisode)