2022-05-10 17:03:41 -05:00
< template >
< div id = "page-wrapper" class = "bg-bg page overflow-y-auto relative" : class = "streamLibraryItem ? 'streaming' : ''" >
2022-11-28 17:49:58 -06:00
< div class = "flex items-center py-4 px-2 md:px-0 max-w-7xl mx-auto" >
2022-05-10 17:03:41 -05:00
< nuxt-link :to = "`/item/${libraryItem.id}`" class = "hover:underline" >
2022-11-28 17:49:58 -06:00
< h1 class = "text-lg lg:text-xl" > { { title } } < / h1 >
2022-05-10 17:03:41 -05:00
< / nuxt-link >
< button class = "w-7 h-7 flex items-center justify-center mx-4 hover:scale-110 duration-100 transform text-gray-200 hover:text-white" @click ="editItem" >
2024-07-08 16:36:37 +00:00
< span class = "material-symbols text-base" > edit < / span >
2022-05-10 17:03:41 -05:00
< / button >
2025-03-16 16:41:37 +02:00
< div class = "grow hidden md:block" / >
2022-11-28 17:49:58 -06:00
< p class = "text-base hidden md:block" > { { $strings . LabelDuration } } : < / p >
< p class = "text-base font-mono ml-4 hidden md:block" > { { $secondsToTimestamp ( mediaDurationRounded ) } } < / p >
2022-05-10 17:03:41 -05:00
< / div >
2022-11-28 17:49:58 -06:00
< div class = "flex flex-wrap-reverse justify-center py-4 px-2" >
2022-05-10 17:03:41 -05:00
< div class = "w-full max-w-3xl py-4" >
< div class = "flex items-center" >
2022-11-28 17:49:58 -06:00
< div class = "w-12 hidden lg:block" / >
2022-11-07 18:27:17 -06:00
< p class = "text-lg mb-4 font-semibold" > { { $strings . HeaderChapters } } < / p >
2025-03-16 16:41:37 +02:00
< div class = "grow" / >
2024-05-13 17:25:01 -05:00
< ui-checkbox v-model = "showSecondInputs" checkbox-bg="primary" small label-class="text-sm text-gray-200 pl-1" :label="$strings.LabelShowSeconds" class="mx-2" / >
2022-11-28 17:49:58 -06:00
< div class = "w-32 hidden lg:block" / >
2022-10-05 18:01:42 -05:00
< / div >
2023-04-09 12:47:36 -05:00
< div class = "flex items-center mb-3 py-1 -mx-1" >
2022-11-28 17:49:58 -06:00
< div class = "w-12 hidden lg:block" / >
2025-03-16 16:41:37 +02:00
< ui-btn v-if = "chapters.length" color="bg-primary" small class="mx-1" @click.stop="removeAllChaptersClick" > {{ $ strings.ButtonRemoveAll }} < / ui -btn >
2023-04-09 12:47:36 -05:00
< ui-btn v-if = "newChapters.length > 1" :color="showShiftTimes ? 'bg' : 'primary'" class="mx-1" small @click="showShiftTimes = !showShiftTimes" > {{ $ strings.ButtonShiftTimes }} < / ui -btn >
2025-03-16 16:41:37 +02:00
< ui-btn color = "bg-primary" small : class = "{ 'mx-1': newChapters.length > 1 }" @ click = "showFindChaptersModal = true" > { { $strings . ButtonLookup } } < / ui-btn >
< div class = "grow" / >
2023-04-09 12:47:36 -05:00
< ui-btn v-if = "hasChanges" small class="mx-1" @click.stop="resetChapters" > {{ $ strings.ButtonReset }} < / ui -btn >
2025-03-16 16:41:37 +02:00
< ui-btn v-if = "hasChanges" color="bg-success" class="mx-1" :disabled="!hasChanges" small @click="saveChapters" > {{ $ strings.ButtonSave }} < / ui -btn >
2022-11-28 17:49:58 -06:00
< div class = "w-32 hidden lg:block" / >
2022-05-10 17:03:41 -05:00
< / div >
2022-10-05 18:01:42 -05:00
< div class = "overflow-hidden" >
< transition name = "slide" >
< div v-if = "showShiftTimes" class="flex mb-4" >
2022-11-28 17:49:58 -06:00
< div class = "w-12 hidden lg:block" / >
2025-03-16 16:41:37 +02:00
< div class = "grow" >
2022-10-05 18:01:42 -05:00
< div class = "flex items-center" >
2022-11-07 18:27:17 -06:00
< p class = "text-sm mb-1 font-semibold pr-2" > { { $strings . LabelTimeToShift } } < / p >
2022-10-05 18:01:42 -05:00
< ui-text-input v-model = "shiftAmount" type="number" class="max-w-20" style="height: 30px" / >
2025-03-16 16:41:37 +02:00
< ui-btn color = "bg-primary" class = "mx-1" small @click ="shiftChapterTimes" > {{ $ strings.ButtonAdd }} < / ui -btn >
< div class = "grow" / >
2024-07-08 16:36:37 +00:00
< span class = "material-symbols text-gray-200 hover:text-white cursor-pointer" @ click = "showShiftTimes = false" > expand _less < / span >
2022-10-05 18:01:42 -05:00
< / div >
2022-11-07 18:27:17 -06:00
< p class = "text-xs py-1.5 text-gray-300 max-w-md" > { { $strings . NoteChapterEditorTimes } } < / p >
2022-10-05 18:01:42 -05:00
< / div >
2022-11-28 17:49:58 -06:00
< div class = "w-32 hidden lg:block" / >
2022-10-05 18:01:42 -05:00
< / div >
< / transition >
< / div >
2022-05-10 17:03:41 -05:00
< div class = "flex text-xs uppercase text-gray-300 font-semibold mb-2" >
2022-11-28 17:49:58 -06:00
< div class = "w-8 min-w-8 md:w-12 md:min-w-12" > < / div >
< div class = "w-24 min-w-24 md:w-32 md:min-w-32 px-2" > { { $strings . LabelStart } } < / div >
2025-03-16 16:41:37 +02:00
< div class = "grow px-2" > { { $strings . LabelTitle } } < / div >
2022-11-28 17:49:58 -06:00
< div class = "w-32" > < / div >
2022-05-10 17:03:41 -05:00
< / div >
< template v-for = "chapter in newChapters" >
< div :key = "chapter.id" class = "flex py-1" >
2022-11-28 17:49:58 -06:00
< div class = "w-8 min-w-8 md:w-12 md:min-w-12" > # { { chapter . id + 1 } } < / div >
< div class = "w-24 min-w-24 md:w-32 md:min-w-32 px-1" >
2022-07-25 18:40:11 -05:00
< ui-text-input v-if = "showSecondInputs" v-model="chapter.start" type="number" class="text-xs" @change="checkChapters" / >
2022-07-25 19:32:04 -05:00
< ui-time-picker v -else class = "text-xs" v-model = "chapter.start" :show-three-digit-hour="mediaDuration >= 360000" @change="checkChapters" / >
2022-05-10 17:03:41 -05:00
< / div >
2025-03-16 16:41:37 +02:00
< div class = "grow px-1" >
2022-11-28 17:49:58 -06:00
< ui-text-input v-model = "chapter.title" @change="checkChapters" class="text-xs" / >
2022-05-10 17:03:41 -05:00
< / div >
2022-11-28 17:49:58 -06:00
< div class = "w-32 min-w-32 px-2 py-1" >
2022-05-10 17:03:41 -05:00
< div class = "flex items-center" >
2022-11-20 18:50:34 +01:00
< ui-tooltip :text = "$strings.MessageRemoveChapter" direction = "bottom" >
< button v-if = "newChapters.length > 1" class="w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-error transform hover:scale-110 duration-150" @click="removeChapter(chapter)" >
2024-08-16 16:57:17 -05:00
< span class = "material-symbols text-base" > remove < / span >
2022-11-20 18:50:34 +01:00
< / button >
< / ui-tooltip >
2022-05-10 17:03:41 -05:00
2022-11-07 18:27:17 -06:00
< ui-tooltip :text = "$strings.MessageInsertChapterBelow" direction = "bottom" >
2022-05-10 17:03:41 -05:00
< button class = "w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-success transform hover:scale-110 duration-150" @click ="addChapter(chapter)" >
2024-07-08 16:36:37 +00:00
< span class = "material-symbols text-lg" > add < / span >
2022-05-10 17:03:41 -05:00
< / button >
< / ui-tooltip >
2022-11-20 18:50:34 +01:00
< ui-tooltip : text = "selectedChapterId === chapter.id && isPlayingChapter ? $strings.MessagePauseChapter : $strings.MessagePlayChapter" direction = "bottom" >
< button class = "w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-white transform hover:scale-110 duration-150" @click ="playChapter(chapter)" >
< widgets-loading-spinner v-if = "selectedChapterId === chapter.id && isLoadingChapter" / >
2024-08-16 16:57:17 -05:00
< span v -else -if = " selectedChapterId = = = chapter.id & & isPlayingChapter " class = "material-symbols text-base" > pause < / span >
< span v -else class = "material-symbols text-base" > play _arrow < / span >
2022-11-20 18:50:34 +01:00
< / button >
< / ui-tooltip >
2022-05-10 17:03:41 -05:00
< ui-tooltip v-if = "chapter.error" :text="chapter.error" direction="left" >
< button class = "w-7 h-7 rounded-full flex items-center justify-center text-error" >
2024-08-16 16:57:17 -05:00
< span class = "material-symbols text-lg" > error _outline < / span >
2022-05-10 17:03:41 -05:00
< / button >
< / ui-tooltip >
< / div >
< / div >
< / div >
< / template >
< / div >
2022-11-28 17:49:58 -06:00
< div class = "w-full max-w-xl py-4 px-2" >
2022-11-28 16:55:13 -06:00
< div class = "flex items-center mb-4 py-1" >
< p class = "text-lg font-semibold" > { { $strings . HeaderAudioTracks } } < / p >
2025-03-16 16:41:37 +02:00
< div class = "grow" / >
2022-11-28 17:00:06 -06:00
< ui-btn small @click ="setChaptersFromTracks" > {{ $ strings.ButtonSetChaptersFromTracks }} < / ui -btn >
< ui-tooltip :text = "$strings.MessageSetChaptersFromTracksDescription" direction = "top" class = "flex items-center mx-1 cursor-default" >
2024-08-16 16:57:17 -05:00
< span class = "material-symbols text-xl text-gray-200" > info < / span >
2022-11-28 16:55:13 -06:00
< / ui-tooltip >
< / div >
2022-05-10 17:03:41 -05:00
< div class = "flex text-xs uppercase text-gray-300 font-semibold mb-2" >
2025-03-16 16:41:37 +02:00
< div class = "grow" > { { $strings . LabelFilename } } < / div >
2022-11-07 18:27:17 -06:00
< div class = "w-20" > { { $strings . LabelDuration } } < / div >
2023-06-23 17:28:35 -05:00
< div class = "w-20 hidden md:block text-center" > { { $strings . HeaderChapters } } < / div >
2022-05-10 17:03:41 -05:00
< / div >
< template v-for = "track in audioTracks" >
2025-03-16 16:41:37 +02:00
< div :key = "track.ino" class = "flex items-center py-2" : class = "currentTrackIndex === track.index && isPlayingChapter ? 'bg-success/10' : ''" >
< div class = "grow max-w-[calc(100%-80px)] pr-2" >
2022-05-10 17:03:41 -05:00
< p class = "text-xs truncate max-w-sm" > { { track . metadata . filename } } < / p >
< / div >
< div class = "w-20" style = "min-width: 80px" >
2022-09-29 17:55:45 -05:00
< p class = "text-xs font-mono text-gray-200" > { { $secondsToTimestamp ( Math . round ( track . duration ) , false , true ) } } < / p >
2022-05-10 17:03:41 -05:00
< / div >
2023-06-23 17:28:35 -05:00
< div class = "w-20 hidden md:flex justify-center" style = "min-width: 80px" >
2024-07-08 16:36:37 +00:00
< span v-if = "(track.chapters || []).length" class="material-symbols text-success text-sm" > check < / span >
2022-05-10 17:03:41 -05:00
< / div >
< / div >
< / template >
< / div >
< / div >
2025-03-16 16:41:37 +02:00
< div v-if = "saving" class="w-full h-full absolute top-0 left-0 bottom-0 right-0 z-30 bg-black/25 flex items-center justify-center" >
2022-05-10 17:03:41 -05:00
< ui-loading-indicator / >
< / div >
< modals-modal v-model ="showFindChaptersModal" name="edit-book" :width="500" :processing ="findingChapters" >
< template # outer >
< div class = "absolute top-0 left-0 p-5 w-2/3 overflow-hidden pointer-events-none" >
2023-02-11 15:02:56 -06:00
< p class = "text-3xl text-white truncate pointer-events-none" > { { $strings . HeaderFindChapters } } < / p >
2022-05-10 17:03:41 -05:00
< / div >
< / template >
< div class = "w-full h-full max-h-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative" >
2025-04-19 23:13:38 -07:00
< div v-if = "!chapterData" class="flex flex-col items-center justify-center p-20" >
< div class = "relative" >
< div class = "flex items-end space-x-2" >
2025-04-27 19:21:37 +02:00
< ui-text-input-with-label v -model .trim = " asinInput " label = "ASIN" class = "flex-3" / >
< ui-dropdown v-model = "regionInput" :label="$strings.LabelRegion" small :items="audibleRegions" class="w-32 flex-2" / >
< ui-btn color = "bg-primary flex-1" @click ="findChapters" > {{ $ strings.ButtonSearch }} < / ui -btn >
< / div >
< div class = "mt-2" >
< ui-checkbox v-model = "removeBranding" label="Remove Audible branding from chapters" small checkbox-bg="bg" label-class="pl-2 text-base text-sm" @click="toggleRemoveBranding" / >
2025-04-19 23:13:38 -07:00
< / div >
< div class = "absolute left-0 mt-1.5 text-error text-s h-5" >
< p v-if = "asinError" > {{ asinError }} < / p >
< p v-if = "asinError" > {{ $ strings.MessageAsinCheck }} < / p >
< / div >
2025-04-27 19:21:37 +02:00
< div class = "invisible mt-1 text-xs" > < / div >
2025-04-19 23:13:38 -07:00
< / div >
2025-04-27 19:21:37 +02:00
2022-05-10 17:03:41 -05:00
< / div >
< div v -else class = "w-full p-4" >
2022-09-29 17:55:45 -05:00
< div class = "flex justify-between mb-4" >
< p >
2022-11-08 17:10:08 -06:00
{ { $strings . LabelDurationFound } } < span class = "font-semibold" > { { $secondsToTimestamp ( chapterData . runtimeLengthSec ) } } < / s p a n
2022-11-02 17:28:26 -05:00
> < br / >
2022-11-08 17:10:08 -06:00
< span class = "font-semibold" : class = "{ 'text-warning': chapters.length !== chapterData.chapters.length }" > { { chapterData . chapters . length } } < / span > { { $strings . LabelChaptersFound } }
2022-09-29 17:55:45 -05:00
< / p >
< p >
2022-11-08 17:10:08 -06:00
{ { $strings . LabelYourAudiobookDuration } } : < span class = "font-semibold" > { { $secondsToTimestamp ( mediaDurationRounded ) } } < / s p a n
2022-11-02 17:28:26 -05:00
> < br / >
Your audiobook has < span class = "font-semibold" : class = "{ 'text-warning': chapters.length !== chapterData.chapters.length }" > { { chapters . length } } < / span > chapters
2022-09-29 17:55:45 -05:00
< / p >
2022-05-10 17:03:41 -05:00
< / div >
2022-11-08 17:10:08 -06:00
< widgets-alert v-if = "chapterData.runtimeLengthSec > mediaDurationRounded" type="warning" class="mb-2" > {{ $ strings.MessageYourAudiobookDurationIsShorter }} < / widgets -alert >
< widgets-alert v -else -if = " chapterData.runtimeLengthSec < mediaDurationRounded " type = "warning" class = "mb-2" > { { $strings . MessageYourAudiobookDurationIsLonger } } < / widgets-alert >
2022-05-10 17:03:41 -05:00
< div class = "flex py-0.5 text-xs font-semibold uppercase text-gray-300 mb-1" >
2022-11-07 18:27:17 -06:00
< div class = "w-24 px-2" > { { $strings . LabelStart } } < / div >
2025-03-16 16:41:37 +02:00
< div class = "grow px-2" > { { $strings . LabelTitle } } < / div >
2022-05-10 17:03:41 -05:00
< / div >
< div class = "w-full max-h-80 overflow-y-auto my-2" >
2025-03-16 16:41:37 +02:00
< div v-for = "(chapter, index) in chapterData.chapters" :key="index" class="flex py-0.5 text-xs" :class="chapter.startOffsetSec > mediaDuration ? 'bg-error/20' : chapter.startOffsetSec + chapter.lengthMs / 1000 > mediaDuration ? 'bg-warning/20' : index % 2 === 0 ? 'bg-primary/30' : ''" >
2022-05-10 17:03:41 -05:00
< div class = "w-24 min-w-24 px-2" >
< p class = "font-mono" > { { $secondsToTimestamp ( chapter . startOffsetSec ) } } < / p >
< / div >
2025-03-16 16:41:37 +02:00
< div class = "grow px-2" >
2022-05-10 17:03:41 -05:00
< p class = "truncate max-w-sm" > { { chapter . title } } < / p >
< / div >
< / div >
< / div >
2022-09-29 17:55:45 -05:00
< div v-if = "chapterData.runtimeLengthSec > mediaDurationRounded" class="w-full pt-2" >
< div class = "flex items-center" >
2025-03-16 16:41:37 +02:00
< div class = "w-2 h-2 bg-warning/50" / >
2022-11-08 17:10:08 -06:00
< p class = "pl-2" > { { $strings . MessageChapterEndIsAfter } } < / p >
2022-09-29 17:55:45 -05:00
< / div >
< div class = "flex items-center" >
2025-03-16 16:41:37 +02:00
< div class = "w-2 h-2 bg-error/50" / >
2022-11-08 17:10:08 -06:00
< p class = "pl-2" > { { $strings . MessageChapterStartIsAfter } } < / p >
2022-09-29 17:55:45 -05:00
< / div >
< / div >
2022-09-29 18:06:13 -05:00
< div class = "flex items-center pt-2" >
2025-03-16 16:41:37 +02:00
< ui-btn small color = "bg-primary" class = "mr-1" @click ="applyChapterNamesOnly" > {{ $ strings.ButtonMapChapterTitles }} < / ui -btn >
2022-11-28 16:55:13 -06:00
< ui-tooltip :text = "$strings.MessageMapChapterTitles" direction = "top" class = "flex items-center" >
2024-08-16 16:57:17 -05:00
< span class = "material-symbols text-xl text-gray-200" > info < / span >
2022-09-29 18:06:13 -05:00
< / ui-tooltip >
2025-03-16 16:41:37 +02:00
< div class = "grow" / >
< ui-btn small color = "bg-success" @click ="applyChapterData" > {{ $ strings.ButtonApplyChapters }} < / ui -btn >
2022-05-10 17:03:41 -05:00
< / div >
< / div >
< / div >
< / modals-modal >
< / div >
< / template >
< script >
2022-11-28 16:55:13 -06:00
import path from 'path'
2022-05-10 17:03:41 -05:00
export default {
2022-07-25 18:57:00 -05:00
async asyncData ( { store , params , app , redirect , from } ) {
2022-05-10 17:03:41 -05:00
if ( ! store . getters [ 'user/getUserCanUpdate' ] ) {
return redirect ( '/?error=unauthorized' )
}
var libraryItem = await app . $axios . $get ( ` /api/items/ ${ params . id } ?expanded=1 ` ) . catch ( ( error ) => {
console . error ( 'Failed' , error )
return false
} )
if ( ! libraryItem ) {
console . error ( 'Not found...' , params . id )
return redirect ( '/' )
}
if ( libraryItem . mediaType != 'book' ) {
console . error ( 'Invalid media type' )
return redirect ( '/' )
}
2022-07-25 18:57:00 -05:00
2025-03-30 17:27:36 -05:00
// Fetch and set library if this items library does not match the current
if ( store . state . libraries . currentLibraryId !== libraryItem . libraryId || ! store . state . libraries . filterData ) {
await store . dispatch ( 'libraries/fetch' , libraryItem . libraryId )
}
2022-07-25 18:57:00 -05:00
var previousRoute = from ? from . fullPath : null
if ( from && from . path === '/login' ) previousRoute = null
2022-05-10 17:03:41 -05:00
return {
2022-07-25 18:57:00 -05:00
libraryItem ,
previousRoute
2022-05-10 17:03:41 -05:00
}
} ,
data ( ) {
return {
newChapters : [ ] ,
selectedChapter : null ,
2022-10-05 18:01:42 -05:00
showShiftTimes : false ,
shiftAmount : 0 ,
2022-05-10 17:03:41 -05:00
audioEl : null ,
isPlayingChapter : false ,
isLoadingChapter : false ,
currentTrackIndex : 0 ,
saving : false ,
asinInput : null ,
2022-10-15 15:31:07 -05:00
regionInput : 'US' ,
2022-05-10 17:03:41 -05:00
findingChapters : false ,
showFindChaptersModal : false ,
2022-07-25 18:40:11 -05:00
chapterData : null ,
2025-04-19 23:13:38 -07:00
asinError : null ,
2025-04-27 19:21:37 +02:00
removeBranding : false ,
2022-10-15 15:31:07 -05:00
showSecondInputs : false ,
2022-11-28 17:49:58 -06:00
audibleRegions : [ 'US' , 'CA' , 'UK' , 'AU' , 'FR' , 'DE' , 'JP' , 'IT' , 'IN' , 'ES' ] ,
hasChanges : false
2022-05-10 17:03:41 -05:00
}
} ,
computed : {
streamLibraryItem ( ) {
return this . $store . state . streamLibraryItem
} ,
userToken ( ) {
return this . $store . getters [ 'user/getToken' ]
} ,
media ( ) {
return this . libraryItem . media || { }
} ,
mediaMetadata ( ) {
return this . media . metadata || { }
} ,
title ( ) {
return this . mediaMetadata . title
} ,
mediaDuration ( ) {
return this . media . duration
} ,
2022-09-29 17:55:45 -05:00
mediaDurationRounded ( ) {
return Math . round ( this . mediaDuration )
} ,
2022-05-10 17:03:41 -05:00
chapters ( ) {
return this . media . chapters || [ ]
} ,
tracks ( ) {
return this . media . tracks || [ ]
} ,
audioFiles ( ) {
return this . media . audioFiles || [ ]
} ,
audioTracks ( ) {
2024-03-21 14:38:52 -05:00
return this . audioFiles . filter ( ( af ) => ! af . exclude )
2022-05-10 17:03:41 -05:00
} ,
selectedChapterId ( ) {
return this . selectedChapter ? this . selectedChapter . id : null
}
} ,
methods : {
2022-11-28 16:55:13 -06:00
setChaptersFromTracks ( ) {
let currentStartTime = 0
let index = 0
const chapters = [ ]
for ( const track of this . audioTracks ) {
chapters . push ( {
id : index ++ ,
title : path . basename ( track . metadata . filename , path . extname ( track . metadata . filename ) ) ,
start : currentStartTime ,
end : currentStartTime + track . duration
} )
currentStartTime += track . duration
}
this . newChapters = chapters
this . checkChapters ( )
} ,
2025-04-27 19:21:37 +02:00
toggleRemoveBranding ( ) {
this . removeBranding = ! this . removeBranding ;
} ,
2022-10-05 18:01:42 -05:00
shiftChapterTimes ( ) {
if ( ! this . shiftAmount || isNaN ( this . shiftAmount ) || this . newChapters . length <= 1 ) {
return
}
const amount = Number ( this . shiftAmount )
const lastChapter = this . newChapters [ this . newChapters . length - 1 ]
if ( lastChapter . start + amount > this . mediaDurationRounded ) {
this . $toast . error ( 'Invalid shift amount. Last chapter start time would extend beyond the duration of this audiobook.' )
return
}
2025-04-27 19:34:12 +02:00
if ( this . newChapters [ 1 ] . start + amount <= 0 ) {
this . $toast . error ( 'Invalid shift amount. The first chapter would have zero or negative length and would be overwritten by the second chapter. Increase the start duration of second chapter. ' )
2022-10-05 18:01:42 -05:00
return
}
for ( let i = 0 ; i < this . newChapters . length ; i ++ ) {
const chap = this . newChapters [ i ]
chap . end = Math . min ( chap . end + amount , this . mediaDuration )
if ( i > 0 ) {
chap . start = Math . max ( 0 , chap . start + amount )
}
}
2023-04-08 14:32:12 -05:00
this . checkChapters ( )
2022-10-05 18:01:42 -05:00
} ,
2022-05-10 17:03:41 -05:00
editItem ( ) {
this . $store . commit ( 'showEditModal' , this . libraryItem )
} ,
addChapter ( chapter ) {
const newChapter = {
id : chapter . id + 1 ,
start : chapter . start ,
end : chapter . end ,
title : ''
}
this . newChapters . splice ( chapter . id + 1 , 0 , newChapter )
this . checkChapters ( )
} ,
removeChapter ( chapter ) {
this . newChapters = this . newChapters . filter ( ( ch ) => ch . id !== chapter . id )
this . checkChapters ( )
} ,
checkChapters ( ) {
2022-11-28 17:49:58 -06:00
let previousStart = 0
let hasChanges = this . newChapters . length !== this . chapters . length
2022-05-10 17:03:41 -05:00
for ( let i = 0 ; i < this . newChapters . length ; i ++ ) {
this . newChapters [ i ] . id = i
this . newChapters [ i ] . start = Number ( this . newChapters [ i ] . start )
2022-12-15 17:40:34 -06:00
this . newChapters [ i ] . title = ( this . newChapters [ i ] . title || '' ) . trim ( )
2022-05-10 17:03:41 -05:00
if ( i === 0 && this . newChapters [ i ] . start !== 0 ) {
2022-11-28 17:49:58 -06:00
this . newChapters [ i ] . error = this . $strings . MessageChapterErrorFirstNotZero
2022-05-10 17:03:41 -05:00
} else if ( this . newChapters [ i ] . start <= previousStart && i > 0 ) {
2022-11-28 17:49:58 -06:00
this . newChapters [ i ] . error = this . $strings . MessageChapterErrorStartLtPrev
2022-05-10 17:03:41 -05:00
} else if ( this . newChapters [ i ] . start >= this . mediaDuration ) {
2022-11-28 17:49:58 -06:00
this . newChapters [ i ] . error = this . $strings . MessageChapterErrorStartGteDuration
2022-05-10 17:03:41 -05:00
} else {
this . newChapters [ i ] . error = null
}
previousStart = this . newChapters [ i ] . start
2022-11-28 17:49:58 -06:00
if ( hasChanges ) {
continue
}
const existingChapter = this . chapters [ i ]
if ( existingChapter ) {
const { start , end , title } = this . newChapters [ i ]
if ( start !== existingChapter . start || end !== existingChapter . end || title !== existingChapter . title ) {
hasChanges = true
}
} else {
hasChanges = true
}
2022-05-10 17:03:41 -05:00
}
2022-11-28 17:49:58 -06:00
this . hasChanges = hasChanges
2022-05-10 17:03:41 -05:00
} ,
playChapter ( chapter ) {
console . log ( 'Play Chapter' , chapter . id )
if ( this . selectedChapterId === chapter . id ) {
console . log ( 'Chapter already playing' , this . isLoadingChapter , this . isPlayingChapter )
if ( this . isLoadingChapter ) return
if ( this . isPlayingChapter ) {
this . destroyAudioEl ( )
return
}
}
if ( this . selectedChapterId ) {
this . destroyAudioEl ( )
}
const audioTrack = this . tracks . find ( ( at ) => {
return chapter . start >= at . startOffset && chapter . start < at . startOffset + at . duration
} )
this . selectedChapter = chapter
this . isLoadingChapter = true
const trackOffset = chapter . start - audioTrack . startOffset
this . playTrackAtTime ( audioTrack , trackOffset )
} ,
playTrackAtTime ( audioTrack , trackOffset ) {
this . currentTrackIndex = audioTrack . index
const audioEl = this . audioEl || document . createElement ( 'audio' )
var src = audioTrack . contentUrl + ` ?token= ${ this . userToken } `
2025-02-01 16:47:36 -06:00
audioEl . src = ` ${ process . env . serverUrl } ${ src } `
2022-05-10 17:03:41 -05:00
audioEl . id = 'chapter-audio'
document . body . appendChild ( audioEl )
audioEl . addEventListener ( 'loadeddata' , ( ) => {
console . log ( 'Audio loaded data' , audioEl . duration )
audioEl . currentTime = trackOffset
audioEl . play ( )
console . log ( 'Playing audio at current time' , trackOffset )
} )
audioEl . addEventListener ( 'play' , ( ) => {
console . log ( 'Audio playing' )
this . isLoadingChapter = false
this . isPlayingChapter = true
} )
audioEl . addEventListener ( 'ended' , ( ) => {
console . log ( 'Audio ended' )
const nextTrack = this . tracks . find ( ( t ) => t . index === this . currentTrackIndex + 1 )
if ( nextTrack ) {
console . log ( 'Playing next track' , nextTrack . index )
this . currentTrackIndex = nextTrack . index
this . playTrackAtTime ( nextTrack , 0 )
} else {
console . log ( 'No next track' )
this . destroyAudioEl ( )
}
} )
this . audioEl = audioEl
} ,
destroyAudioEl ( ) {
if ( ! this . audioEl ) return
this . audioEl . remove ( )
this . audioEl = null
this . selectedChapter = null
this . isPlayingChapter = false
this . isLoadingChapter = false
} ,
saveChapters ( ) {
this . checkChapters ( )
for ( let i = 0 ; i < this . newChapters . length ; i ++ ) {
if ( this . newChapters [ i ] . error ) {
2022-11-28 17:49:58 -06:00
this . $toast . error ( this . $strings . ToastChaptersHaveErrors )
2022-05-10 17:03:41 -05:00
return
}
if ( ! this . newChapters [ i ] . title ) {
2022-11-28 17:49:58 -06:00
this . $toast . error ( this . $strings . ToastChaptersMustHaveTitles )
2022-05-10 17:03:41 -05:00
return
}
const nextChapter = this . newChapters [ i + 1 ]
if ( nextChapter ) {
this . newChapters [ i ] . end = nextChapter . start
} else {
this . newChapters [ i ] . end = this . mediaDuration
}
}
this . saving = true
const payload = {
chapters : this . newChapters
}
this . $axios
. $post ( ` /api/items/ ${ this . libraryItem . id } /chapters ` , payload )
. then ( ( data ) => {
this . saving = false
if ( data . updated ) {
2024-10-17 15:03:08 -07:00
this . $toast . success ( this . $strings . ToastChaptersUpdated )
2022-07-25 18:57:00 -05:00
if ( this . previousRoute ) {
this . $router . push ( this . previousRoute )
} else {
this . $router . push ( ` /item/ ${ this . libraryItem . id } ` )
}
2022-05-10 17:03:41 -05:00
} else {
2022-11-08 17:10:08 -06:00
this . $toast . info ( this . $strings . MessageNoUpdatesWereNecessary )
2022-05-10 17:03:41 -05:00
}
} )
. catch ( ( error ) => {
this . saving = false
console . error ( 'Failed to update chapters' , error )
2024-09-29 17:53:52 -05:00
this . $toast . error ( this . $strings . ToastFailedToUpdate )
2022-05-10 17:03:41 -05:00
} )
} ,
2022-09-29 18:06:13 -05:00
applyChapterNamesOnly ( ) {
this . newChapters . forEach ( ( chapter , index ) => {
if ( this . chapterData . chapters [ index ] ) {
chapter . title = this . chapterData . chapters [ index ] . title
}
} )
this . showFindChaptersModal = false
this . chapterData = null
2022-12-01 17:42:02 -06:00
this . checkChapters ( )
2022-09-29 18:06:13 -05:00
} ,
2022-05-10 17:03:41 -05:00
applyChapterData ( ) {
2022-12-15 17:40:34 -06:00
let index = 0
2022-05-10 17:03:41 -05:00
this . newChapters = this . chapterData . chapters
. filter ( ( chap ) => chap . startOffsetSec < this . mediaDuration )
. map ( ( chap ) => {
return {
id : index ++ ,
start : chap . startOffsetMs / 1000 ,
2022-12-15 17:40:34 -06:00
end : Math . min ( this . mediaDuration , ( chap . startOffsetMs + chap . lengthMs ) / 1000 ) ,
2022-05-10 17:03:41 -05:00
title : chap . title
}
} )
this . showFindChaptersModal = false
this . chapterData = null
2022-12-01 17:42:02 -06:00
this . checkChapters ( )
2022-05-10 17:03:41 -05:00
} ,
findChapters ( ) {
if ( ! this . asinInput ) {
2024-10-17 15:03:08 -07:00
this . $toast . error ( this . $strings . ToastAsinRequired )
2022-05-10 17:03:41 -05:00
return
}
2022-10-15 15:31:07 -05:00
// Update local storage region
if ( this . regionInput !== localStorage . getItem ( 'audibleRegion' ) ) {
localStorage . setItem ( 'audibleRegion' , this . regionInput )
}
2022-05-10 17:03:41 -05:00
this . findingChapters = true
this . chapterData = null
2025-04-19 23:13:38 -07:00
this . asinError = null // used to show warning about audible vs amazon ASIN
2022-05-10 17:03:41 -05:00
this . $axios
2022-10-15 15:31:07 -05:00
. $get ( ` /api/search/chapters?asin= ${ this . asinInput } ®ion= ${ this . regionInput } ` )
2022-05-10 17:03:41 -05:00
. then ( ( data ) => {
this . findingChapters = false
if ( data . error ) {
2025-04-19 23:25:17 -07:00
this . asinError = this . $getString ( data . stringKey )
2022-05-10 17:03:41 -05:00
} else {
console . log ( 'Chapter data' , data )
2025-04-27 19:21:37 +02:00
this . chapterData = this . removeBranding ? this . removeBrandingFromData ( data ) : data
2022-05-10 17:03:41 -05:00
}
} )
. catch ( ( error ) => {
this . findingChapters = false
console . error ( 'Failed to get chapter data' , error )
2024-08-30 15:47:49 -07:00
this . $toast . error ( this . $strings . ToastFailedToLoadData )
2022-05-10 17:03:41 -05:00
this . showFindChaptersModal = false
} )
2022-11-28 17:49:58 -06:00
} ,
2025-04-27 19:21:37 +02:00
removeBrandingFromData ( data ) {
if ( ! data ) return data
try {
const introDuration = data . brandIntroDurationMs
const outroDuration = data . brandOutroDurationMs
for ( let i = 0 ; i < data . chapters . length ; i ++ ) {
const chapter = data . chapters [ i ]
if ( chapter . startOffsetMs < introDuration ) {
// This should never happen, as the intro is not longer than the first chapter
// If this happens set to the next second
// Will be 0 for the first chapter anayways
chapter . startOffsetMs = i * 1000
chapter . startOffsetSec = i
} else {
chapter . startOffsetMs -= introDuration
chapter . startOffsetSec = Math . floor ( chapter . startOffsetMs / 1000 )
}
}
const lastChapter = data . chapters [ data . chapters . length - 1 ]
// If there is an outro that's in the outro duration, remove it
if ( lastChapter && lastChapter . lengthMs <= outroDuration ) {
data . chapters . pop ( )
}
} catch {
return data
}
return data
} ,
2022-11-28 17:49:58 -06:00
resetChapters ( ) {
const payload = {
message : this . $strings . MessageResetChaptersConfirm ,
callback : ( confirmed ) => {
if ( confirmed ) {
this . initChapters ( )
}
} ,
type : 'yesNo'
}
this . $store . commit ( 'globals/setConfirmPrompt' , payload )
} ,
initChapters ( ) {
this . newChapters = this . chapters . map ( ( c ) => ( { ... c } ) )
if ( ! this . newChapters . length ) {
this . newChapters = [
{
id : 0 ,
start : 0 ,
end : this . mediaDuration ,
title : ''
}
]
}
this . checkChapters ( )
2023-04-09 12:47:36 -05:00
} ,
removeAllChaptersClick ( ) {
const payload = {
message : this . $strings . MessageConfirmRemoveAllChapters ,
callback : ( confirmed ) => {
if ( confirmed ) {
this . removeAllChapters ( )
}
} ,
type : 'yesNo'
}
this . $store . commit ( 'globals/setConfirmPrompt' , payload )
} ,
removeAllChapters ( ) {
this . saving = true
const payload = {
chapters : [ ]
}
this . $axios
. $post ( ` /api/items/ ${ this . libraryItem . id } /chapters ` , payload )
. then ( ( data ) => {
if ( data . updated ) {
2024-08-30 15:47:49 -07:00
this . $toast . success ( this . $strings . ToastChaptersRemoved )
2023-04-09 12:47:36 -05:00
if ( this . previousRoute ) {
this . $router . push ( this . previousRoute )
} else {
this . $router . push ( ` /item/ ${ this . libraryItem . id } ` )
}
} else {
this . $toast . info ( this . $strings . MessageNoUpdatesWereNecessary )
}
} )
. catch ( ( error ) => {
console . error ( 'Failed to remove chapters' , error )
2024-08-30 15:47:49 -07:00
this . $toast . error ( this . $strings . ToastRemoveFailed )
2023-04-09 12:47:36 -05:00
} )
. finally ( ( ) => {
this . saving = false
} )
2024-09-19 17:21:41 -05:00
} ,
libraryItemUpdated ( libraryItem ) {
if ( libraryItem . id === this . libraryItem . id ) {
if ( ! ! libraryItem . media . metadata . asin && this . mediaMetadata . asin !== libraryItem . media . metadata . asin ) {
this . asinInput = libraryItem . media . metadata . asin
}
this . libraryItem = libraryItem
}
2022-05-10 17:03:41 -05:00
}
} ,
mounted ( ) {
2022-10-15 15:31:07 -05:00
this . regionInput = localStorage . getItem ( 'audibleRegion' ) || 'US'
2022-05-10 17:03:41 -05:00
this . asinInput = this . mediaMetadata . asin || null
2022-11-28 17:49:58 -06:00
this . initChapters ( )
2024-09-19 17:21:41 -05:00
this . $eventBus . $on ( ` ${ this . libraryItem . id } _updated ` , this . libraryItemUpdated )
2022-08-28 15:11:14 -05:00
} ,
beforeDestroy ( ) {
this . destroyAudioEl ( )
2024-09-19 17:21:41 -05:00
this . $eventBus . $off ( ` ${ this . libraryItem . id } _updated ` , this . libraryItemUpdated )
2022-05-10 17:03:41 -05:00
}
}
2024-05-13 17:25:01 -05:00
< / script >