diff --git a/client/components/app/MediaPlayerContainer.vue b/client/components/app/MediaPlayerContainer.vue
index 1a2b1d30a..3564a5670 100644
--- a/client/components/app/MediaPlayerContainer.vue
+++ b/client/components/app/MediaPlayerContainer.vue
@@ -280,9 +280,11 @@ export default {
this.playerHandler.playPause()
},
jumpForward() {
+ this._manualSeekTime = Date.now()
this.playerHandler.jumpForward()
},
jumpBackward() {
+ this._manualSeekTime = Date.now()
this.playerHandler.jumpBackward()
},
setVolume(volume) {
@@ -293,6 +295,7 @@ export default {
this.playerHandler.setPlaybackRate(playbackRate)
},
seek(time) {
+ this._manualSeekTime = Date.now()
this.playerHandler.seek(time)
},
playbackTimeUpdate(time) {
@@ -308,6 +311,9 @@ export default {
if (this.sleepTimerType === this.$constants.SleepTimerTypes.CHAPTER && this.sleepTimerSet) {
this.checkChapterEnd()
}
+
+ // check for intro/outro and skip if needed
+ this.checkAndSkipIntroOutro(time)
},
setDuration(duration) {
this.totalDuration = duration
@@ -543,6 +549,87 @@ export default {
this.playerHandler.resetPlayer() // Closes player without reporting to server
this.$store.commit('setMediaPlaying', null)
}
+ },
+
+ // get skip settings
+ getSkipSettings() {
+ return {
+ skipIntro: this.$store.getters['user/getUserSetting']('skipIntro'),
+ introDuration: this.$store.getters['user/getUserSetting']('introDuration'),
+ skipOutro: this.$store.getters['user/getUserSetting']('skipOutro'),
+ outroDuration: this.$store.getters['user/getUserSetting']('outroDuration')
+ }
+ },
+
+ // check and skip intro/outro
+ checkAndSkipIntroOutro(currentTime) {
+ const skipSettings = this.getSkipSettings()
+ if (!skipSettings) return
+
+ const doSkipIntro = skipSettings.skipIntro && skipSettings.introDuration > 0
+ const doSkipOutro = skipSettings.skipOutro && skipSettings.outroDuration > 0
+ if (!doSkipIntro && !doSkipOutro) return
+ if (!this.isPlaying || !this.chapters.length) return
+
+ // The skip function is not triggered within 2 seconds after the user manually seeks
+ if (this._manualSeekTime && Date.now() - this._manualSeekTime < 2000) return
+
+ // Reentry guard: When skipping, wait until reaching the target position before cancelling
+ if (this._isSkipping) {
+ if (this._skipTarget != null && currentTime >= this._skipTarget - 0.5) {
+ this._isSkipping = false
+ this._skipTarget = null
+ }
+ return
+ }
+
+ const chapter = this.chapters.find((ch) => ch.start <= currentTime && currentTime < ch.end)
+ if (!chapter) return
+
+ const introDuration = doSkipIntro ? skipSettings.introDuration : 0
+ const outroDuration = doSkipOutro ? skipSettings.outroDuration : 0
+
+ const introEndTime = Math.min(chapter.start + introDuration, chapter.end)
+ const outroStartTime = Math.max(chapter.end - outroDuration, chapter.start)
+
+ // Short chapter: If the intro and outro intervals overlap, do not skip
+ if (doSkipIntro && doSkipOutro && introEndTime > outroStartTime) return
+
+ // Check whether it is within the intro interval
+ if (doSkipIntro && currentTime < introEndTime) {
+ const target = introEndTime + 0.5
+ this._isSkipping = true
+ this._skipTarget = target
+ this.seek(target)
+ return
+ }
+
+ // check whether it is within the outro interval
+ if (doSkipOutro && currentTime >= outroStartTime) {
+ const chapterIndex = this.chapters.indexOf(chapter)
+ const nextChapter = this.chapters[chapterIndex + 1]
+
+ if (nextChapter) {
+ // has next chapter: skip to next chapter start (if skipIntro is on, skip intro)
+ let target = nextChapter.start
+ if (doSkipIntro) {
+ const nextIntroEnd = Math.min(nextChapter.start + introDuration, nextChapter.end)
+ const nextOutroStart = Math.max(nextChapter.end - outroDuration, nextChapter.start)
+ // ensure that the next chapter intro/outro does not overlap
+ if (nextIntroEnd <= nextOutroStart) {
+ target = nextIntroEnd + 0.5
+ }
+ }
+ this._isSkipping = true
+ this._skipTarget = target
+ this.seek(target)
+ } else {
+ // last chapter: skip to end
+ this._isSkipping = true
+ this._skipTarget = chapter.end
+ this.seek(chapter.end)
+ }
+ }
}
},
mounted() {
diff --git a/client/components/modals/PlayerSettingsModal.vue b/client/components/modals/PlayerSettingsModal.vue
index dfac28cfd..4f7ca30f1 100644
--- a/client/components/modals/PlayerSettingsModal.vue
+++ b/client/components/modals/PlayerSettingsModal.vue
@@ -17,6 +17,28 @@
+
+
+
{{ $strings.HeaderChapterIntroOutroSkipSettings }}
+
+
+
+
+ {{ $strings.LabelSkipChapterIntro }}
+
+
+
{{ $strings.LabelSeconds }}
+
+
+
+
+
+ {{ $strings.LabelSkipChapterOutro }}
+
+
+
{{ $strings.LabelSeconds }}
+
+
@@ -40,7 +62,11 @@ export default {
jumpForwardAmount: 10,
jumpBackwardAmount: 10,
playbackRateIncrementDecrementValues: [0.1, 0.05],
- playbackRateIncrementDecrement: 0.1
+ playbackRateIncrementDecrement: 0.1,
+ skipIntro: false,
+ introDuration: 10,
+ skipOutro: false,
+ outroDuration: 10
}
},
computed: {
@@ -69,11 +95,29 @@ export default {
this.playbackRateIncrementDecrement = val
this.$store.dispatch('user/updateUserSettings', { playbackRateIncrementDecrement: val })
},
+ setSkipIntro() {
+ this.$store.dispatch('user/updateUserSettings', { skipIntro: this.skipIntro })
+ },
+ setIntroDuration() {
+ this.introDuration = Math.max(0, Math.min(60, parseInt(this.introDuration) || 0))
+ this.$store.dispatch('user/updateUserSettings', { introDuration: this.introDuration })
+ },
+ setSkipOutro() {
+ this.$store.dispatch('user/updateUserSettings', { skipOutro: this.skipOutro })
+ },
+ setOutroDuration() {
+ this.outroDuration = Math.max(0, Math.min(60, parseInt(this.outroDuration) || 0))
+ this.$store.dispatch('user/updateUserSettings', { outroDuration: this.outroDuration })
+ },
settingsUpdated() {
this.useChapterTrack = this.$store.getters['user/getUserSetting']('useChapterTrack')
this.jumpForwardAmount = this.$store.getters['user/getUserSetting']('jumpForwardAmount')
this.jumpBackwardAmount = this.$store.getters['user/getUserSetting']('jumpBackwardAmount')
this.playbackRateIncrementDecrement = this.$store.getters['user/getUserSetting']('playbackRateIncrementDecrement')
+ this.skipIntro = this.$store.getters['user/getUserSetting']('skipIntro') || false
+ this.introDuration = this.$store.getters['user/getUserSetting']('introDuration') || 10
+ this.skipOutro = this.$store.getters['user/getUserSetting']('skipOutro') || false
+ this.outroDuration = this.$store.getters['user/getUserSetting']('outroDuration') || 10
}
},
mounted() {
diff --git a/client/store/user.js b/client/store/user.js
index 96e79d12f..d702dd386 100644
--- a/client/store/user.js
+++ b/client/store/user.js
@@ -18,7 +18,11 @@ export const state = () => ({
authorSortBy: 'name',
authorSortDesc: false,
jumpForwardAmount: 10,
- jumpBackwardAmount: 10
+ jumpBackwardAmount: 10,
+ skipIntro: false,
+ introDuration: 10,
+ skipOutro: false,
+ outroDuration: 10
}
})
diff --git a/client/strings/en-us.json b/client/strings/en-us.json
index fb2bcb281..a636d41b9 100644
--- a/client/strings/en-us.json
+++ b/client/strings/en-us.json
@@ -129,6 +129,7 @@
"HeaderBackups": "Backups",
"HeaderBulkChapterModal": "Add Multiple Chapters",
"HeaderChangePassword": "Change Password",
+ "HeaderChapterIntroOutroSkipSettings": "Intro/Outro Skip Settings",
"HeaderChapters": "Chapters",
"HeaderChooseAFolder": "Choose a Folder",
"HeaderCollection": "Collection",
@@ -567,6 +568,7 @@
"LabelSearchTitleOrASIN": "Search Title or ASIN",
"LabelSeason": "Season",
"LabelSeasonNumber": "Season #{0}",
+ "LabelSeconds": "seconds",
"LabelSelectAll": "Select all",
"LabelSelectAllEpisodes": "Select all episodes",
"LabelSelectEpisodesShowing": "Select {0} episodes showing",
@@ -629,6 +631,8 @@
"LabelShowSeconds": "Show seconds",
"LabelShowSubtitles": "Show Subtitles",
"LabelSize": "Size",
+ "LabelSkipChapterIntro": "Skip Chapter Intro",
+ "LabelSkipChapterOutro": "Skip Chapter Outro",
"LabelSleepTimer": "Sleep timer",
"LabelSlug": "Slug",
"LabelSortAscending": "Ascending",
diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json
index 14c70cb10..4b32c0fb2 100644
--- a/client/strings/zh-cn.json
+++ b/client/strings/zh-cn.json
@@ -129,6 +129,7 @@
"HeaderBackups": "备份",
"HeaderBulkChapterModal": "添加多个章节",
"HeaderChangePassword": "更改密码",
+ "HeaderChapterIntroOutroSkipSettings": "片头 / 片尾跳过设置",
"HeaderChapters": "章节",
"HeaderChooseAFolder": "选择文件夹",
"HeaderCollection": "收藏",
@@ -567,6 +568,7 @@
"LabelSearchTitleOrASIN": "搜索标题或 ASIN",
"LabelSeason": "季",
"LabelSeasonNumber": "第 {0} 季",
+ "LabelSeconds": "秒",
"LabelSelectAll": "全选",
"LabelSelectAllEpisodes": "选择所有剧集",
"LabelSelectEpisodesShowing": "选择正在播放的 {0} 剧集",
@@ -629,6 +631,8 @@
"LabelShowSeconds": "显示秒数",
"LabelShowSubtitles": "显示标题",
"LabelSize": "文件大小",
+ "LabelSkipChapterIntro": "跳过章节片头",
+ "LabelSkipChapterOutro": "跳过章节片尾",
"LabelSleepTimer": "睡眠定时",
"LabelSlug": "Slug",
"LabelSortAscending": "升序",