From f54e5748e4524a63e53ada2dc6a4885167f35273 Mon Sep 17 00:00:00 2001 From: Martin Tillmann Date: Tue, 21 Jan 2025 20:05:45 +0100 Subject: [PATCH] bump version to 0.1.4, add Audible format support and corresponding tests --- audible-chapter-spec.md | 90 ++++++++++ package-lock.json | 272 ++++++++++++++++++++--------- package.json | 2 +- readme.md | 51 +++--- src/Formats/Audible.ts | 77 ++++++++ src/Formats/AutoFormat.ts | 4 +- tests/conversions.test.ts | 10 +- tests/format_audible.test.ts | 69 ++++++++ tests/format_autodetection.test.ts | 5 +- tests/samples/audible.json | 127 ++++++++++++++ 10 files changed, 593 insertions(+), 114 deletions(-) create mode 100644 audible-chapter-spec.md create mode 100644 src/Formats/Audible.ts create mode 100644 tests/format_audible.test.ts create mode 100644 tests/samples/audible.json diff --git a/audible-chapter-spec.md b/audible-chapter-spec.md new file mode 100644 index 0000000..5b291e3 --- /dev/null +++ b/audible-chapter-spec.md @@ -0,0 +1,90 @@ +# Audible Chapter Spec + +This document describes the structure of an Audible chapter format that is used +by audible's web player and presumably their mobile apps. + +The format is part of an endpoint that audible class `licenserequest` which contains other information about the book and the current user's license, which is is out of the scope of this document. + +```json +{ + "content_license": { + "content_metadata": { + "chapter_info": { + "brandIntroDurationMs": 2043, + "brandOutroDurationMs": 5061, + "chapters": [ + { + "length_ms": 15154, + "start_offset_ms": 0, + "start_offset_sec": 0, + "title": "Opening Credits" + }, + { + "length_ms": 3450888, + "start_offset_ms": 15154, + "start_offset_sec": 15, + "title": "Introduction" + }, + { + "length_ms": 7019855, + "start_offset_ms": 3466042, + "start_offset_sec": 3466, + "title": "Chapter 1." + }, + { + "length_ms": 10525325, + "start_offset_ms": 10485897, + "start_offset_sec": 10486, + "title": "Chapter 2." + }, + { + "length_ms": 8016827, + "start_offset_ms": 21011222, + "start_offset_sec": 21011, + "title": "Chapter 3." + }, + { + "length_ms": 6340115, + "start_offset_ms": 29028049, + "start_offset_sec": 29028, + "title": "Chapter 4." + }, + { + "length_ms": 8660485, + "start_offset_ms": 35368164, + "start_offset_sec": 35368, + "title": "Chapter 5." + }, + { + "length_ms": 6277049, + "start_offset_ms": 44028649, + "start_offset_sec": 44029, + "title": "Chapter 6." + }, + { + "length_ms": 1765645, + "start_offset_ms": 50305698, + "start_offset_sec": 50306, + "title": "Conclusion" + }, + { + "length_ms": 379599, + "start_offset_ms": 52071343, + "start_offset_sec": 52071, + "title": "Acknowledgments" + }, + { + "length_ms": 23033, + "start_offset_ms": 52450942, + "start_offset_sec": 52451, + "title": "End Credits" + } + ], + "is_accurate": true, + "runtime_length_ms": 52473975, + "runtime_length_sec": 52474 + } + } + } +} +``` \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index ab1efa1..af9ad5f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,13 @@ { "name": "@mtillmann/chapters", - "version": "1.0.0", + "version": "0.1.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@mtillmann/chapters", - "version": "1.0.0", - "license": "ISC", + "version": "0.1.3", + "license": "MIT", "dependencies": { "filenamify": "^6.0.0", "jsdom": "^24.0.0" @@ -2951,169 +2951,266 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.12.0.tgz", - "integrity": "sha512-+ac02NL/2TCKRrJu2wffk1kZ+RyqxVUlbjSagNgPm94frxtr+XDL12E5Ll1enWskLrtrZ2r8L3wED1orIibV/w==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.31.0.tgz", + "integrity": "sha512-9NrR4033uCbUBRgvLcBrJofa2KY9DzxL2UKZ1/4xA/mnTNyhZCWBuD8X3tPm1n4KxcgaraOYgrFKSgwjASfmlA==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.12.0.tgz", - "integrity": "sha512-OBqcX2BMe6nvjQ0Nyp7cC90cnumt8PXmO7Dp3gfAju/6YwG0Tj74z1vKrfRz7qAv23nBcYM8BCbhrsWqO7PzQQ==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.31.0.tgz", + "integrity": "sha512-iBbODqT86YBFHajxxF8ebj2hwKm1k8PTBQSojSt3d1FFt1gN+xf4CowE47iN0vOSdnd+5ierMHBbu/rHc7nq5g==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.12.0.tgz", - "integrity": "sha512-X64tZd8dRE/QTrBIEs63kaOBG0b5GVEd3ccoLtyf6IdXtHdh8h+I56C2yC3PtC9Ucnv0CpNFJLqKFVgCYe0lOQ==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.31.0.tgz", + "integrity": "sha512-WHIZfXgVBX30SWuTMhlHPXTyN20AXrLH4TEeH/D0Bolvx9PjgZnn4H677PlSGvU6MKNsjCQJYczkpvBbrBnG6g==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.12.0.tgz", - "integrity": "sha512-cc71KUZoVbUJmGP2cOuiZ9HSOP14AzBAThn3OU+9LcA1+IUqswJyR1cAJj3Mg55HbjZP6OLAIscbQsQLrpgTOg==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.31.0.tgz", + "integrity": "sha512-hrWL7uQacTEF8gdrQAqcDy9xllQ0w0zuL1wk1HV8wKGSGbKPVjVUv/DEwT2+Asabf8Dh/As+IvfdU+H8hhzrQQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.31.0.tgz", + "integrity": "sha512-S2oCsZ4hJviG1QjPY1h6sVJLBI6ekBeAEssYKad1soRFv3SocsQCzX6cwnk6fID6UQQACTjeIMB+hyYrFacRew==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.31.0.tgz", + "integrity": "sha512-pCANqpynRS4Jirn4IKZH4tnm2+2CqCNLKD7gAdEjzdLGbH1iO0zouHz4mxqg0uEMpO030ejJ0aA6e1PJo2xrPA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.12.0.tgz", - "integrity": "sha512-a6w/Y3hyyO6GlpKL2xJ4IOh/7d+APaqLYdMf86xnczU3nurFTaVN9s9jOXQg97BE4nYm/7Ga51rjec5nfRdrvA==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.31.0.tgz", + "integrity": "sha512-0O8ViX+QcBd3ZmGlcFTnYXZKGbFu09EhgD27tgTdGnkcYXLat4KIsBBQeKLR2xZDCXdIBAlWLkiXE1+rJpCxFw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.31.0.tgz", + "integrity": "sha512-w5IzG0wTVv7B0/SwDnMYmbr2uERQp999q8FMkKG1I+j8hpPX2BYFjWe69xbhbP6J9h2gId/7ogesl9hwblFwwg==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.12.0.tgz", - "integrity": "sha512-0fZBq27b+D7Ar5CQMofVN8sggOVhEtzFUwOwPppQt0k+VR+7UHMZZY4y+64WJ06XOhBTKXtQB/Sv0NwQMXyNAA==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.31.0.tgz", + "integrity": "sha512-JyFFshbN5xwy6fulZ8B/8qOqENRmDdEkcIMF0Zz+RsfamEW+Zabl5jAb0IozP/8UKnJ7g2FtZZPEUIAlUSX8cA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.12.0.tgz", - "integrity": "sha512-eTvzUS3hhhlgeAv6bfigekzWZjaEX9xP9HhxB0Dvrdbkk5w/b+1Sxct2ZuDxNJKzsRStSq1EaEkVSEe7A7ipgQ==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.31.0.tgz", + "integrity": "sha512-kpQXQ0UPFeMPmPYksiBL9WS/BDiQEjRGMfklVIsA0Sng347H8W2iexch+IEwaR7OVSKtr2ZFxggt11zVIlZ25g==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.31.0.tgz", + "integrity": "sha512-pMlxLjt60iQTzt9iBb3jZphFIl55a70wexvo8p+vVFK+7ifTRookdoXX3bOsRdmfD+OKnMozKO6XM4zR0sHRrQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.31.0.tgz", + "integrity": "sha512-D7TXT7I/uKEuWiRkEFbed1UUYZwcJDU4vZQdPTcepK7ecPhzKOYk4Er2YR4uHKme4qDeIh6N3XrLfpuM7vzRWQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.12.0.tgz", - "integrity": "sha512-ix+qAB9qmrCRiaO71VFfY8rkiAZJL8zQRXveS27HS+pKdjwUfEhqo2+YF2oI+H/22Xsiski+qqwIBxVewLK7sw==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.31.0.tgz", + "integrity": "sha512-wal2Tc8O5lMBtoePLBYRKj2CImUCJ4UNGJlLwspx7QApYny7K1cUYlzQ/4IGQBLmm+y0RS7dwc3TDO/pmcneTw==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.31.0.tgz", + "integrity": "sha512-O1o5EUI0+RRMkK9wiTVpk2tyzXdXefHtRTIjBbmFREmNMy7pFeYXCFGbhKFwISA3UOExlo5GGUuuj3oMKdK6JQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.12.0.tgz", - "integrity": "sha512-TenQhZVOtw/3qKOPa7d+QgkeM6xY0LtwzR8OplmyL5LrgTWIXpTQg2Q2ycBf8jm+SFW2Wt/DTn1gf7nFp3ssVA==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.31.0.tgz", + "integrity": "sha512-zSoHl356vKnNxwOWnLd60ixHNPRBglxpv2g7q0Cd3Pmr561gf0HiAcUBRL3S1vPqRC17Zo2CX/9cPkqTIiai1g==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.12.0.tgz", - "integrity": "sha512-LfFdRhNnW0zdMvdCb5FNuWlls2WbbSridJvxOvYWgSBOYZtgBfW9UGNJG//rwMqTX1xQE9BAodvMH9tAusKDUw==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.31.0.tgz", + "integrity": "sha512-ypB/HMtcSGhKUQNiFwqgdclWNRrAYDH8iMYH4etw/ZlGwiTVxBz2tDrGRrPlfZu6QjXwtd+C3Zib5pFqID97ZA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.12.0.tgz", - "integrity": "sha512-JPDxovheWNp6d7AHCgsUlkuCKvtu3RB55iNEkaQcf0ttsDU/JZF+iQnYcQJSk/7PtT4mjjVG8N1kpwnI9SLYaw==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.31.0.tgz", + "integrity": "sha512-JuhN2xdI/m8Hr+aVO3vspO7OQfUFO6bKLIRTAy0U15vmWjnZDLrEgCZ2s6+scAYaQVpYSh9tZtRijApw9IXyMw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.12.0.tgz", - "integrity": "sha512-fjtuvMWRGJn1oZacG8IPnzIV6GF2/XG+h71FKn76OYFqySXInJtseAqdprVTDTyqPxQOG9Exak5/E9Z3+EJ8ZA==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.31.0.tgz", + "integrity": "sha512-U1xZZXYkvdf5MIWmftU8wrM5PPXzyaY1nGCI4KI4BFfoZxHamsIe+BtnPLIvvPykvQWlVbqUXdLa4aJUuilwLQ==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.12.0.tgz", - "integrity": "sha512-ZYmr5mS2wd4Dew/JjT0Fqi2NPB/ZhZ2VvPp7SmvPZb4Y1CG/LRcS6tcRo2cYU7zLK5A7cdbhWnnWmUjoI4qapg==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.31.0.tgz", + "integrity": "sha512-ul8rnCsUumNln5YWwz0ted2ZHFhzhRRnkpBZ+YRuHoRAlUji9KChpOUOndY7uykrPEPXVbHLlsdo6v5yXo/TXw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -3194,10 +3291,11 @@ } }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" }, "node_modules/@types/graceful-fs": { "version": "4.1.9", @@ -4074,12 +4172,13 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, + "license": "MIT", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -4552,10 +4651,11 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -5812,10 +5912,11 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -6614,6 +6715,7 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -8136,12 +8238,13 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -8943,12 +9046,13 @@ } }, "node_modules/rollup": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.12.0.tgz", - "integrity": "sha512-wz66wn4t1OHIJw3+XU7mJJQV/2NAfw5OAk6G6Hoo3zcvz/XOfQ52Vgi+AN4Uxoxi0KBBwk2g8zPrTDA4btSB/Q==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.31.0.tgz", + "integrity": "sha512-9cCE8P4rZLx9+PjoyqHLs31V9a9Vpvfo4qNcs6JCiGWYhw2gijSetFbH6SSy1whnkgcefnUwr8sad7tgqsGvnw==", "dev": true, + "license": "MIT", "dependencies": { - "@types/estree": "1.0.5" + "@types/estree": "1.0.6" }, "bin": { "rollup": "dist/bin/rollup" @@ -8958,19 +9062,25 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.12.0", - "@rollup/rollup-android-arm64": "4.12.0", - "@rollup/rollup-darwin-arm64": "4.12.0", - "@rollup/rollup-darwin-x64": "4.12.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.12.0", - "@rollup/rollup-linux-arm64-gnu": "4.12.0", - "@rollup/rollup-linux-arm64-musl": "4.12.0", - "@rollup/rollup-linux-riscv64-gnu": "4.12.0", - "@rollup/rollup-linux-x64-gnu": "4.12.0", - "@rollup/rollup-linux-x64-musl": "4.12.0", - "@rollup/rollup-win32-arm64-msvc": "4.12.0", - "@rollup/rollup-win32-ia32-msvc": "4.12.0", - "@rollup/rollup-win32-x64-msvc": "4.12.0", + "@rollup/rollup-android-arm-eabi": "4.31.0", + "@rollup/rollup-android-arm64": "4.31.0", + "@rollup/rollup-darwin-arm64": "4.31.0", + "@rollup/rollup-darwin-x64": "4.31.0", + "@rollup/rollup-freebsd-arm64": "4.31.0", + "@rollup/rollup-freebsd-x64": "4.31.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.31.0", + "@rollup/rollup-linux-arm-musleabihf": "4.31.0", + "@rollup/rollup-linux-arm64-gnu": "4.31.0", + "@rollup/rollup-linux-arm64-musl": "4.31.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.31.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.31.0", + "@rollup/rollup-linux-riscv64-gnu": "4.31.0", + "@rollup/rollup-linux-s390x-gnu": "4.31.0", + "@rollup/rollup-linux-x64-gnu": "4.31.0", + "@rollup/rollup-linux-x64-musl": "4.31.0", + "@rollup/rollup-win32-arm64-msvc": "4.31.0", + "@rollup/rollup-win32-ia32-msvc": "4.31.0", + "@rollup/rollup-win32-x64-msvc": "4.31.0", "fsevents": "~2.3.2" } }, @@ -9562,6 +9672,7 @@ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -10244,9 +10355,10 @@ } }, "node_modules/ws": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", - "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", "engines": { "node": ">=10.0.0" }, diff --git a/package.json b/package.json index f48e13c..e957cb6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@mtillmann/chapters", - "version": "0.1.3", + "version": "0.1.4", "description": "library that manages and converts chapters of multiple formats", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/readme.md b/readme.md index 50c077b..b996926 100644 --- a/readme.md +++ b/readme.md @@ -10,25 +10,26 @@ This is the core library of the [chaptertool](https://github.com/Mtillmann/chapt ## Supported formats -| class | description | key | ext | info | -|-------------------------------------|------------------------------|----------------|--------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| ChaptersJson | Podcasting 2.0 Chapters | chaptersjson | `json` | [spec](https://github.com/Podcastindex-org/podcast-namespace/blob/main/chapters/jsonChapters.md) | -| FFMetadata | FFMetadata | ffmpegdata | `txt` | [spec](https://ffmpeg.org/ffmpeg-formats.html#Metadata-1) | -| MatroskaXML | Matroska XML chapters | matroskaxml | `xml` | [spec](https://www.matroska.org/technical/chapters.html) | -| MKVMergeXML | MKVToolNix mkvmerge XML | mkvmergexml | `xml` | [spec](https://mkvtoolnix.download/doc/mkvmerge.html#mkvmerge.chapters) | -| MKVMergeSimple | MKVToolNix mkvmerge _simple_ | mkvmergesimple | `txt` | [spec](https://mkvtoolnix.download/doc/mkvmerge.html#mkvmerge.chapters) | -| WebVTT | WebVTT Chapters | webvtt | `vtt` | [spec](https://developer.mozilla.org/en-US/docs/Web/API/WebVTT_API) | -| Youtube | Youtube Chapter Syntax | youtube | `txt` | | -| FFMpegInfo | FFMpegInfo | ffmpeginfo | `txt` | read only, used internally | -| PySceneDetect | PySceneDetect | pyscenedetect | `csv` | [project home](https://github.com/Breakthrough/PySceneDetect) | -| VorbisComment | Vorbis Comment Format | vorbiscomment | `txt` | [spec](https://wiki.xiph.org/Chapter_Extension) | -| AppleChapters | "Apple Chapters" | applechapters | `xml` | [source](https://github.com/rigaya/NVEnc/blob/master/NVEncC_Options.en.md#--chapter-string:~:text=CHAPTER03NAME%3Dchapter%2D3-,apple%20format,-(should%20be%20in)) | -| ShutterEDL | Shutter EDL | edl | `edl` | [source](https://github.com/paulpacifico/shutter-encoder/blob/f3d6bb6dfcd629861a0b0a50113bf4b062e1ba17/src/application/SceneDetection.java) | +| class | description | key | ext | info | +| --------------------- | ---------------------------- | -------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| ChaptersJson | Podcasting 2.0 Chapters | chaptersjson | `json` | [spec](https://github.com/Podcastindex-org/podcast-namespace/blob/main/chapters/jsonChapters.md) | +| FFMetadata | FFMetadata | ffmpegdata | `txt` | [spec](https://ffmpeg.org/ffmpeg-formats.html#Metadata-1) | +| MatroskaXML | Matroska XML chapters | matroskaxml | `xml` | [spec](https://www.matroska.org/technical/chapters.html) | +| MKVMergeXML | MKVToolNix mkvmerge XML | mkvmergexml | `xml` | [spec](https://mkvtoolnix.download/doc/mkvmerge.html#mkvmerge.chapters) | +| MKVMergeSimple | MKVToolNix mkvmerge _simple_ | mkvmergesimple | `txt` | [spec](https://mkvtoolnix.download/doc/mkvmerge.html#mkvmerge.chapters) | +| WebVTT | WebVTT Chapters | webvtt | `vtt` | [spec](https://developer.mozilla.org/en-US/docs/Web/API/WebVTT_API) | +| Youtube | Youtube Chapter Syntax | youtube | `txt` | | +| FFMpegInfo | FFMpegInfo | ffmpeginfo | `txt` | read only, used internally | +| PySceneDetect | PySceneDetect | pyscenedetect | `csv` | [project home](https://github.com/Breakthrough/PySceneDetect) | +| VorbisComment | Vorbis Comment Format | vorbiscomment | `txt` | [spec](https://wiki.xiph.org/Chapter_Extension) | +| AppleChapters | "Apple Chapters" | applechapters | `xml` | [source](https://github.com/rigaya/NVEnc/blob/master/NVEncC_Options.en.md#--chapter-string:~:text=CHAPTER03NAME%3Dchapter%2D3-,apple%20format,-(should%20be%20in)) | +| ShutterEDL | Shutter EDL | edl | `edl` | [source](https://github.com/paulpacifico/shutter-encoder/blob/f3d6bb6dfcd629861a0b0a50113bf4b062e1ba17/src/application/SceneDetection.java) | | PodloveSimpleChapters | Podlove Simple Chapters | psc | `xml` | [spec](https://podlove.org/simple-chapters/) | -| PodloveJson | Podlove Simple Chapters JSON | podlovejson | `json` | [source](https://github.com/podlove/chapters#:~:text=org/%3E-,Encode%20to%20JSON,-iex%3E%20Chapters) | -| MP4Chaps | MP4Chaps | mp4chaps | `txt` | [source](https://github.com/podlove/chapters#:~:text=%3Achapters%3E-,Encode%20to%20mp4chaps,-iex%3E%20Chapters) | -| AppleHLS | Apple HLS Chapters | applehls | `json` | [spec](https://developer.apple.com/documentation/http-live-streaming/providing-javascript-object-notation-json-chapters), partial support | -| Scenecut | Scenecut format | scenecut | `json` | [source](https://github.com/slhck/scenecut-extractor#:~:text=cuts%20in%20JSON-,format,-%3A) | +| PodloveJson | Podlove Simple Chapters JSON | podlovejson | `json` | [source](https://github.com/podlove/chapters#:~:text=org/%3E-,Encode%20to%20JSON,-iex%3E%20Chapters) | +| MP4Chaps | MP4Chaps | mp4chaps | `txt` | [source](https://github.com/podlove/chapters#:~:text=%3Achapters%3E-,Encode%20to%20mp4chaps,-iex%3E%20Chapters) | +| AppleHLS | Apple HLS Chapters | applehls | `json` | [spec](https://developer.apple.com/documentation/http-live-streaming/providing-javascript-object-notation-json-chapters), partial support | +| Scenecut | Scenecut format | scenecut | `json` | [source](https://github.com/slhck/scenecut-extractor#:~:text=cuts%20in%20JSON-,format,-%3A) | +| Audible | Audible Chapter Format | audible | `json` | [source](./audible-chapter-spec.md) | ## Installation @@ -163,28 +164,28 @@ Some formats support additional options: #### ChapterJson toString() options | option | type | default | description | -|---------------------|-----------|---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| ------------------- | --------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `imagePrefix` | `string` | `''` | Prefix for image URLs | | `writeRedundantToc` | `boolean` | `false` | Write [redundant](https://github.com/Podcastindex-org/podcast-namespace/blob/main/chapters/jsonChapters.md#:~:text=or%20not%20present%20at%20all) TOC attributes | | `writeEndTimes` | `boolean` | `false` | Write end times | #### AppleChapters toString() options -| option | type | default | description | -|-----------------|-----------|---------|-----------------------------------------------------------| -| `acUseTextAttr` | `boolean` | `false` | When set, the text-attribute will be used instead of node textContent | +| option | type | default | description | +| --------------- | --------- | ------- | ---------------------------------------------------------------------- | +| `acUseTextAttr` | `boolean` | `false` | When set, the text-attribute will be used instead of node textContent | #### PySceneDetect toString() options | option | type | default | description | -|--------------------|----------|----------|------------------------------------------| +| ------------------ | -------- | -------- | ---------------------------------------- | | `psdFramerate` | `number` | `23.976` | Framerate of the video file | | `psdOmitTimecodes` | `boolen` | `false` | When set, the first line will be omitted | #### Scenecut toString() options -| option | type | default | description | -|--------|----------|---------|-----------------------------| +| option | type | default | description | +| ----------- | -------- | ------- | --------------------------- | | `frameRate` | `number` | `30` | Framerate of the video file | | `ptsScale` | `number` | `1` | PTS scale (See below) | | `score` | `number` | `0.5` | Score threshold | diff --git a/src/Formats/Audible.ts b/src/Formats/Audible.ts new file mode 100644 index 0000000..2a8dc38 --- /dev/null +++ b/src/Formats/Audible.ts @@ -0,0 +1,77 @@ +import { type Chapter } from '../Types/Chapter' +import { Base } from './Base' + +export class Audible extends Base { + filename = 'audible-chapters.json' + mimeType = 'application/json' + + test (data: Record): { errors: string[] } { + if (!('content_license' in data)) { + return { errors: ['JSON Structure: key content_license missing'] } + } + + if (!('content_metadata' in data.content_license)) { + return { errors: ['JSON Structure: key content_license.content_metadata missing'] } + } + + if (!('chapter_info' in data.content_license.content_metadata)) { + return { errors: ['JSON Structure: key content_license.content_metadata.chapter_info missing'] } + } + + if (!('chapters' in data.content_license.content_metadata.chapter_info)) { + return { errors: ['JSON Structure: key content_license.content_metadata.chapter_info.chapters missing'] } + } + + if (!Array.isArray(data.content_license.content_metadata.chapter_info.chapters)) { + return { errors: ['JSON Structure: content_license.content_metadata.chapter_info.chapters must be an array'] } + } + + if (!data.content_license.content_metadata.chapter_info.chapters.every((chapter: Record) => 'start_offset_sec' in chapter)) { + return { errors: ['JSON Structure: every chapter must have a start property'] } + } + + return { errors: [] } + } + + parse (string: string): void { + const data = JSON.parse(string) + const { errors } = this.test(data as object[]) + if (errors.length > 0) { + throw new Error(errors.join('')) + } + + this.duration = data.content_license.content_metadata.chapter_info.runtime_length_ms + + this.chapters = data.content_license.content_metadata.chapter_info.chapters.map((raw: any) => { + const { start_offset_ms: startTime, title } = raw + const chapter: Chapter = { + startTime + } + if (title) { + chapter.title = title + } + return chapter + }) + + this.bump() + } + + toString (pretty = false): string { + return JSON.stringify({ + content_license: { + content_metadata: { + chapter_info: { + brandIntroDurationMs: 2043, + brandOutroDurationMs: 5061, + chapters: this.chapters.map((chapter, i) => ({ + length_ms: Math.round(chapter.duration!), + start_offset_ms: chapter.startTime, + start_offset_sec: Math.round(chapter.startTime / 1000), + title: this.ensureTitle(i) + })) + } + } + } + }, null, pretty ? 2 : 0) + } +} diff --git a/src/Formats/AutoFormat.ts b/src/Formats/AutoFormat.ts index 28ba506..8fb6aa2 100644 --- a/src/Formats/AutoFormat.ts +++ b/src/Formats/AutoFormat.ts @@ -15,6 +15,7 @@ import { MP4Chaps } from './MP4Chaps' import { PodloveJson } from './PodloveJson' import { AppleHLS } from './AppleHLS' import { Scenecut } from './Scenecut' +import { Audible } from './Audible' import { type MediaItem } from '../Types/MediaItem' const classMap: Record = { @@ -34,7 +35,8 @@ const classMap: Record = { mp4chaps: MP4Chaps, podlovejson: PodloveJson, applehls: AppleHLS, - scenecut: Scenecut + scenecut: Scenecut, + audible: Audible } export const AutoFormat = { diff --git a/tests/conversions.test.ts b/tests/conversions.test.ts index a631a7c..8866df9 100644 --- a/tests/conversions.test.ts +++ b/tests/conversions.test.ts @@ -14,16 +14,16 @@ import { MP4Chaps } from "../src/Formats/MP4Chaps"; import { PodloveJson } from "../src/Formats/PodloveJson"; import { AppleHLS } from "../src/Formats/AppleHLS"; import { Scenecut } from "../src/Formats/Scenecut"; +import { Audible } from "../src/Formats/Audible"; import { readFileSync } from "fs"; import { sep } from "path"; describe('conversions from one format to any other', () => { const formats = [ - ChaptersJson, WebVTT, Youtube, FFMetadata, - MatroskaXML, MKVMergeXML, MKVMergeSimple, - PySceneDetect, AppleChapters, ShutterEDL, - VorbisComment, PodloveSimpleChapters, MP4Chaps, - PodloveJson, AppleHLS, Scenecut + AppleChapters, AppleHLS, Audible, ChaptersJson, + FFMetadata, MatroskaXML, MKVMergeSimple, MKVMergeXML, + MP4Chaps, PodloveJson, PodloveSimpleChapters, PySceneDetect, + Scenecut, ShutterEDL, VorbisComment, WebVTT, Youtube ]; const content = readFileSync(module.path + sep + 'samples' + sep + 'chapters.json', 'utf-8'); diff --git a/tests/format_audible.test.ts b/tests/format_audible.test.ts new file mode 100644 index 0000000..194d0ed --- /dev/null +++ b/tests/format_audible.test.ts @@ -0,0 +1,69 @@ + +import { readFileSync } from "fs"; +import { sep } from "path"; +import { ShutterEDL } from "../src/Formats/ShutterEDL"; +import { Audible } from "../src/Formats/Audible"; + + +describe('Audible Format Handler', () => { + it('accepts no arguments', () => { + expect(() => { + Audible.create(); + }).not.toThrow(TypeError); + }); + + + it('fails on malformed input', () => { + expect(() => { + Audible.create('asdf'); + }).toThrow(Error); + }); + + const content = readFileSync(module.path + sep + 'samples' + sep + 'audible.json', 'utf-8'); + + it('parses well-formed input', () => { + expect(() => { + Audible.create(content); + }).not.toThrow(Error); + }); + + const instance = Audible.create(content); + + it('has the correct number of chapters from content', () => { + expect(instance.chapters.length).toEqual(11); + }); + + it('has parsed the timestamps correctly', () => { + expect(instance.chapters[1].startTime).toBe(15154) + }); + + it('has parsed the chapter titles correctly', () => { + expect(instance.chapters[0].title).toBe('Opening Credits') + }); + + it('exports to correct format', () => { + expect(instance.toString()).toContain('start_offset_ms":'); + }); + + it('export includes correct timestamp', () => { + expect(instance.toString()).toContain('35368164'); + }); + + it('can import previously generated export', () => { + expect(Audible.create(instance.toString()).chapters[2].startTime).toEqual(3466042); + }); + + it('can convert into other format', () => { + expect(instance.to(ShutterEDL)).toBeInstanceOf(ShutterEDL) + }); + + it('will apply the pretty print option', () => { + expect(instance.toString(true)).not.toEqual(instance.toString()); + }); + + it('fails on empty input array', () => { + expect(() => { + (new Audible).from(JSON.stringify([])); + }).toThrow(Error); + }); +}); diff --git a/tests/format_autodetection.test.ts b/tests/format_autodetection.test.ts index e6c1bf6..a732ecc 100644 --- a/tests/format_autodetection.test.ts +++ b/tests/format_autodetection.test.ts @@ -18,12 +18,14 @@ import { VorbisComment } from "../src/Formats/VorbisComment"; import { PodloveJson } from "../src/Formats/PodloveJson"; import { Scenecut } from "../src/Formats/Scenecut"; import { AppleHLS } from "../src/Formats/AppleHLS"; +import { Audible } from "../src/Formats/Audible"; describe('autodetection of sample files', () => { const filesAndKeysAndHandlers = [ ['applechapters.xml', 'applechapters', AppleChapters], ['applehls.json', 'applehls', AppleHLS], + ['audible.json', 'audible', Audible], ['chapters.json', 'chaptersjson', ChaptersJson], ['FFMetadata.txt', 'ffmetadata', FFMetadata], ['ffmpeginfo.txt', 'ffmpeginfo', FFMpegInfo], @@ -38,8 +40,7 @@ describe('autodetection of sample files', () => { ['shutter.edl', 'shutteredl', ShutterEDL], ['vorbiscomment.txt', 'vorbiscomment', VorbisComment], ['webvtt.txt', 'webvtt', WebVTT], - ['youtube-chapters.txt', 'youtube', Youtube], - + ['youtube-chapters.txt', 'youtube', Youtube], ]; diff --git a/tests/samples/audible.json b/tests/samples/audible.json new file mode 100644 index 0000000..f733048 --- /dev/null +++ b/tests/samples/audible.json @@ -0,0 +1,127 @@ +{ + "content_license": { + "acr": "", + "asin": "", + "certificate": "", + "content_metadata": { + "chapter_info": { + "brandIntroDurationMs": 2043, + "brandOutroDurationMs": 5061, + "chapters": [ + { + "length_ms": 15154, + "start_offset_ms": 0, + "start_offset_sec": 0, + "title": "Opening Credits" + }, + { + "length_ms": 3450888, + "start_offset_ms": 15154, + "start_offset_sec": 15, + "title": "Introduction" + }, + { + "length_ms": 7019855, + "start_offset_ms": 3466042, + "start_offset_sec": 3466, + "title": "Chapter 1." + }, + { + "length_ms": 10525325, + "start_offset_ms": 10485897, + "start_offset_sec": 10486, + "title": "Chapter 2." + }, + { + "length_ms": 8016827, + "start_offset_ms": 21011222, + "start_offset_sec": 21011, + "title": "Chapter 3." + }, + { + "length_ms": 6340115, + "start_offset_ms": 29028049, + "start_offset_sec": 29028, + "title": "Chapter 4." + }, + { + "length_ms": 8660485, + "start_offset_ms": 35368164, + "start_offset_sec": 35368, + "title": "Chapter 5." + }, + { + "length_ms": 6277049, + "start_offset_ms": 44028649, + "start_offset_sec": 44029, + "title": "Chapter 6." + }, + { + "length_ms": 1765645, + "start_offset_ms": 50305698, + "start_offset_sec": 50306, + "title": "Conclusion" + }, + { + "length_ms": 379599, + "start_offset_ms": 52071343, + "start_offset_sec": 52071, + "title": "Acknowledgments" + }, + { + "length_ms": 23033, + "start_offset_ms": 52450942, + "start_offset_sec": 52451, + "title": "End Credits" + } + ], + "is_accurate": true, + "runtime_length_ms": 52473975, + "runtime_length_sec": 52474 + }, + "content_reference": { + "acr": "", + "asin": "", + "codec": "mp4a.40.2", + "content_format": "M4A_AAX", + "content_size_in_bytes": 234540516, + "file_version": "1", + "marketplace": "", + "sku": "", + "tempo": "1.0", + "version": "67171021" + }, + "last_position_heard": { + "last_updated": "2025-01-21 16:13:05.523", + "position_ms": 47330663, + "status": "Exists" + } + }, + "drm_type": "Widevine", + "granted_right": "Ownership", + "license_id": "", + "license_response": "", + "license_response_type": "FullTitle", + "message": "Ownership: User [] has [Purchase] rights.", + "playback_info": { + "end_offset_ms": 52473975, + "last_position_heard": { + "last_updated": "2025-01-21T16:13:05Z", + "position_ms": 47330663, + "status": "Exists" + }, + "start_offset_ms": 0 + }, + "preview": false, + "request_id": "", + "requires_ad_supported_playback": false, + "status_code": "Granted" + }, + "response_groups": [ + "always-returned", + "last_position_heard", + "certificate", + "chapter_info", + "content_reference" + ] +} \ No newline at end of file