diff --git a/package-lock.json b/package-lock.json
index d55583a5..a6c46eb7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13739,8 +13739,7 @@
"node-forge": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz",
- "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==",
- "dev": true
+ "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA=="
},
"node-gyp": {
"version": "7.1.2",
diff --git a/package.json b/package.json
index 35687839..7d0ff59f 100644
--- a/package.json
+++ b/package.json
@@ -28,6 +28,7 @@
"markdown": "^0.5.0",
"material-design-icons": "^3.0.1",
"nedb": "^1.8.0",
+ "node-forge": "^0.10.0",
"opml-to-json": "^1.0.1",
"rss-parser": "^3.12.0",
"socks-proxy-agent": "^5.0.0",
diff --git a/src/renderer/components/ft-video-player/ft-video-player.js b/src/renderer/components/ft-video-player/ft-video-player.js
index bab6a169..5da1bbba 100644
--- a/src/renderer/components/ft-video-player/ft-video-player.js
+++ b/src/renderer/components/ft-video-player/ft-video-player.js
@@ -66,6 +66,10 @@ export default Vue.extend({
thumbnail: {
type: String,
default: ''
+ },
+ videoId: {
+ type: String,
+ required: true
}
},
data: function () {
@@ -149,6 +153,14 @@ export default Vue.extend({
autoplayVideos: function () {
return this.$store.getters.getAutoplayVideos
+ },
+
+ useSponsorBlock: function () {
+ return this.$store.getters.getUseSponsorBlock
+ },
+
+ sponsorBlockShowSkippedToast: function () {
+ return this.$store.getters.getSponsorBlockShowSkippedToast
}
},
mounted: function () {
@@ -274,6 +286,109 @@ export default Vue.extend({
}
})
}
+ setTimeout(() => { this.fetchSponsorBlockInfo() }, 100)
+ },
+
+ fetchSponsorBlockInfo() {
+ if (this.useSponsorBlock) {
+ this.$store.dispatch('sponsorBlockSkipSegments', {
+ videoId: this.videoId,
+ categories: ['sponsor']
+ }).then((skipSegments) => {
+ this.player.on('timeupdate', () => {
+ this.skipSponsorBlocks(skipSegments)
+ })
+ skipSegments.forEach(({
+ category,
+ segment: [startTime, endTime]
+ }) => {
+ this.addSponsorBlockMarker({
+ time: startTime,
+ duration: endTime - startTime,
+ color: this.sponsorBlockCategoryColor(category)
+ })
+ })
+ })
+ }
+ },
+
+ skipSponsorBlocks(skipSegments) {
+ const currentTime = this.player.currentTime()
+ let newTime = null
+ let skippedCategory = null
+ skipSegments.forEach(({ category, segment: [startTime, endTime] }) => {
+ if (startTime <= currentTime && currentTime < endTime) {
+ newTime = endTime
+ skippedCategory = category
+ }
+ })
+ if (newTime !== null) {
+ if (this.sponsorBlockShowSkippedToast) {
+ this.showSkippedSponsorSegmentInformation(skippedCategory)
+ }
+ this.player.currentTime(newTime)
+ }
+ },
+
+ showSkippedSponsorSegmentInformation(category) {
+ const translatedCategory = this.sponsorBlockTranslatedCategory(category)
+ this.showToast({
+ message: `${this.$t('Video.Skipped segment')} ${translatedCategory}`
+ })
+ },
+
+ sponsorBlockTranslatedCategory(category) {
+ switch (category) {
+ case 'sponsor':
+ return this.$t('Video.Sponsor Block category.sponsor')
+ case 'intro':
+ return this.$t('Video.Sponsor Block category.intro')
+ case 'outro':
+ return this.$t('Video.Sponsor Block category.outro')
+ case 'selfpromo':
+ return this.$t('Video.Sponsor Block category.self-promotion')
+ case 'interaction':
+ return this.$t('Video.Sponsor Block category.interaction')
+ case 'music_offtopic':
+ return this.$t('Video.Sponsor Block category.music offtopic')
+ default:
+ console.error(`Unknown translation for SponsorBlock category ${category}`)
+ return category
+ }
+ },
+
+ sponsorBlockCategoryColor(category) {
+ // TODO: allow to set these colors in settings
+ switch (category) {
+ case 'sponsor':
+ return '#00d400'
+ case 'intro':
+ return '#00ffff'
+ case 'outro':
+ return '#0202ed'
+ case 'selfpromo':
+ return '#ffff00'
+ case 'interaction':
+ return '#cc00ff'
+ case 'music_offtopic':
+ return '#ff9900'
+ default:
+ console.error(`Unknown SponsorBlock category ${category}`)
+ return 'yellow'
+ }
+ },
+
+ addSponsorBlockMarker(marker) {
+ const markerDiv = videojs.dom.createEl('div', {}, {})
+
+ markerDiv.className = 'sponsorBlockMarker'
+ markerDiv.style.height = '100%'
+ markerDiv.style.position = 'absolute'
+ markerDiv.style['background-color'] = marker.color
+ markerDiv.style.width = (marker.duration / this.player.duration()) * 100 + '%'
+ markerDiv.style.marginLeft = (marker.time / this.player.duration()) * 100 + '%'
+
+ this.player.el().querySelector('.vjs-progress-holder').appendChild(markerDiv)
},
checkAspectRatio() {
@@ -1186,6 +1301,7 @@ export default Vue.extend({
},
...mapActions([
+ 'showToast',
'calculateColorLuminance'
])
}
diff --git a/src/renderer/components/sponsor-block-settings/sponsor-block-settings.css b/src/renderer/components/sponsor-block-settings/sponsor-block-settings.css
new file mode 100644
index 00000000..97e780cc
--- /dev/null
+++ b/src/renderer/components/sponsor-block-settings/sponsor-block-settings.css
@@ -0,0 +1,25 @@
+.relative {
+ position: relative;
+}
+
+.card {
+ width: 85%;
+ margin: 0 auto;
+ margin-bottom: 10px;
+}
+
+.center {
+ text-align: center;
+}
+
+@media only screen and (max-width: 680px) {
+ .card {
+ width: 90%;
+ }
+}
+
+@media only screen and (max-width: 500px) {
+ .sponsorBlockSettingsFlexBox {
+ justify-content: flex-start;
+ }
+}
diff --git a/src/renderer/components/sponsor-block-settings/sponsor-block-settings.js b/src/renderer/components/sponsor-block-settings/sponsor-block-settings.js
new file mode 100644
index 00000000..45fd4623
--- /dev/null
+++ b/src/renderer/components/sponsor-block-settings/sponsor-block-settings.js
@@ -0,0 +1,48 @@
+import Vue from 'vue'
+import { mapActions } from 'vuex'
+import FtCard from '../ft-card/ft-card.vue'
+import FtToggleSwitch from '../ft-toggle-switch/ft-toggle-switch.vue'
+import FtInput from '../ft-input/ft-input.vue'
+import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
+
+export default Vue.extend({
+ name: 'SponsorBlockSettings',
+ components: {
+ 'ft-card': FtCard,
+ 'ft-toggle-switch': FtToggleSwitch,
+ 'ft-input': FtInput,
+ 'ft-flex-box': FtFlexBox
+ },
+ computed: {
+ useSponsorBlock: function () {
+ return this.$store.getters.getUseSponsorBlock
+ },
+ sponsorBlockUrl: function () {
+ return this.$store.getters.getSponsorBlockUrl
+ },
+ sponsorBlockShowSkippedToast: function () {
+ return this.$store.getters.getSponsorBlockShowSkippedToast
+ }
+ },
+ methods: {
+ handleUpdateSponsorBlock: function (value) {
+ this.updateUseSponsorBlock(value)
+ },
+
+ handleUpdateSponsorBlockUrl: function (value) {
+ const sponsorBlockUrlWithoutTrailingSlash = value.replace(/\/$/, '')
+ const sponsorBlockUrlWithoutApiSuffix = sponsorBlockUrlWithoutTrailingSlash.replace(/\/api$/, '')
+ this.updateSponsorBlockUrl(sponsorBlockUrlWithoutApiSuffix)
+ },
+
+ handleUpdateSponsorBlockShowSkippedToast: function (value) {
+ this.updateSponsorBlockShowSkippedToast(value)
+ },
+
+ ...mapActions([
+ 'updateUseSponsorBlock',
+ 'updateSponsorBlockUrl',
+ 'updateSponsorBlockShowSkippedToast'
+ ])
+ }
+})
diff --git a/src/renderer/components/sponsor-block-settings/sponsor-block-settings.vue b/src/renderer/components/sponsor-block-settings/sponsor-block-settings.vue
new file mode 100644
index 00000000..e57d62ed
--- /dev/null
+++ b/src/renderer/components/sponsor-block-settings/sponsor-block-settings.vue
@@ -0,0 +1,37 @@
+
+
+
+ {{ $t("Settings.SponsorBlock Settings.SponsorBlock Settings") }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/renderer/store/modules/settings.js b/src/renderer/store/modules/settings.js
index bd12ed7c..a53a8666 100644
--- a/src/renderer/store/modules/settings.js
+++ b/src/renderer/store/modules/settings.js
@@ -78,7 +78,10 @@ const state = {
hidePopularVideos: false,
hidePlaylists: false,
hideLiveChat: false,
- hideActiveSubscriptions: false
+ hideActiveSubscriptions: false,
+ useSponsorBlock: false,
+ sponsorBlockUrl: 'https://sponsor.ajay.app',
+ sponsorBlockShowSkippedToast: true
}
const getters = {
@@ -264,6 +267,18 @@ const getters = {
getHideActiveSubscriptions: () => {
return state.hideActiveSubscriptions
+ },
+
+ getUseSponsorBlock: () => {
+ return state.useSponsorBlock
+ },
+
+ getSponsorBlockUrl: () => {
+ return state.sponsorBlockUrl
+ },
+
+ getSponsorBlockShowSkippedToast: () => {
+ return state.sponsorBlockShowSkippedToast
}
}
@@ -417,6 +432,14 @@ const actions = {
case 'hideActiveSubscriptions':
commit('setHideActiveSubscriptions', result.value)
break
+ case 'useSponsorBlock':
+ commit('setUseSponsorBlock', result.value)
+ break
+ case 'sponsorBlockUrl':
+ commit('setSponsorBlockUrl', result.value)
+ break
+ case 'sponsorBlockShowSkippedToast':
+ commit('setSponsorBlockShowSkippedToast', result.value)
}
})
resolve()
@@ -786,6 +809,30 @@ const actions = {
commit('setHideLiveChat', hideLiveChat)
}
})
+ },
+
+ updateUseSponsorBlock ({ commit }, useSponsorBlock) {
+ settingsDb.update({ _id: 'useSponsorBlock' }, { _id: 'useSponsorBlock', value: useSponsorBlock }, { upsert: true }, (err, numReplaced) => {
+ if (!err) {
+ commit('setUseSponsorBlock', useSponsorBlock)
+ }
+ })
+ },
+
+ updateSponsorBlockUrl ({ commit }, sponsorBlockUrl) {
+ settingsDb.update({ _id: 'sponsorBlockUrl' }, { _id: 'sponsorBlockUrl', value: sponsorBlockUrl }, { upsert: true }, (err, numReplaced) => {
+ if (!err) {
+ commit('setSponsorBlockUrl', sponsorBlockUrl)
+ }
+ })
+ },
+
+ updateSponsorBlockShowSkippedToast ({ commit }, sponsorBlockShowSkippedToast) {
+ settingsDb.update({ _id: 'sponsorBlockShowSkippedToast' }, { _id: 'sponsorBlockShowSkippedToast', value: sponsorBlockShowSkippedToast }, { upsert: true }, (err, numReplaced) => {
+ if (!err) {
+ commit('setSponsorBlockShowSkippedToast', sponsorBlockShowSkippedToast)
+ }
+ })
}
}
@@ -944,6 +991,15 @@ const mutations = {
},
setHideActiveSubscriptions (state, hideActiveSubscriptions) {
state.hideActiveSubscriptions = hideActiveSubscriptions
+ },
+ setUseSponsorBlock (state, useSponsorBlock) {
+ state.useSponsorBlock = useSponsorBlock
+ },
+ setSponsorBlockUrl (state, sponsorBlockUrl) {
+ state.sponsorBlockUrl = sponsorBlockUrl
+ },
+ setSponsorBlockShowSkippedToast (state, sponsorBlockShowSkippedToast) {
+ state.sponsorBlockShowSkippedToast = sponsorBlockShowSkippedToast
}
}
diff --git a/src/renderer/store/modules/sponsorblock.js b/src/renderer/store/modules/sponsorblock.js
new file mode 100644
index 00000000..bfcbd224
--- /dev/null
+++ b/src/renderer/store/modules/sponsorblock.js
@@ -0,0 +1,38 @@
+import $ from 'jquery'
+import forge from 'node-forge'
+
+const state = {}
+const getters = {}
+
+const actions = {
+ sponsorBlockSkipSegments ({ rootState }, { videoId, categories }) {
+ return new Promise((resolve, reject) => {
+ const messageDigestSha256 = forge.md.sha256.create()
+ messageDigestSha256.update(videoId)
+ const videoIdHashPrefix = messageDigestSha256.digest().toHex().substring(0, 4)
+ const requestUrl = `${rootState.settings.sponsorBlockUrl}/api/skipSegments/${videoIdHashPrefix}?categories=${JSON.stringify(categories)}`
+
+ $.getJSON(requestUrl, (response) => {
+ const segments = response
+ .filter((result) => result.videoID === videoId)
+ .flatMap((result) => result.segments)
+ resolve(segments)
+ }).fail((xhr, textStatus, error) => {
+ console.log(xhr)
+ console.log(textStatus)
+ console.log(requestUrl)
+ console.log(error)
+ reject(xhr)
+ })
+ })
+ }
+}
+
+const mutations = {}
+
+export default {
+ state,
+ getters,
+ actions,
+ mutations
+}
diff --git a/src/renderer/views/Settings/Settings.js b/src/renderer/views/Settings/Settings.js
index 11b0b2c3..d0c78bf2 100644
--- a/src/renderer/views/Settings/Settings.js
+++ b/src/renderer/views/Settings/Settings.js
@@ -9,6 +9,7 @@ import PrivacySettings from '../../components/privacy-settings/privacy-settings.
import DataSettings from '../../components/data-settings/data-settings.vue'
import DistractionSettings from '../../components/distraction-settings/distraction-settings.vue'
import ProxySettings from '../../components/proxy-settings/proxy-settings.vue'
+import SponsorBlockSettings from '../../components/sponsor-block-settings/sponsor-block-settings.vue'
export default Vue.extend({
name: 'Settings',
@@ -22,6 +23,7 @@ export default Vue.extend({
'privacy-settings': PrivacySettings,
'data-settings': DataSettings,
'distraction-settings': DistractionSettings,
- 'proxy-settings': ProxySettings
+ 'proxy-settings': ProxySettings,
+ 'sponsor-block-settings': SponsorBlockSettings
}
})
diff --git a/src/renderer/views/Settings/Settings.vue b/src/renderer/views/Settings/Settings.vue
index 33a1fb35..9fbf0585 100644
--- a/src/renderer/views/Settings/Settings.vue
+++ b/src/renderer/views/Settings/Settings.vue
@@ -8,6 +8,7 @@
+
diff --git a/src/renderer/views/Watch/Watch.vue b/src/renderer/views/Watch/Watch.vue
index e325435b..449caaae 100644
--- a/src/renderer/views/Watch/Watch.vue
+++ b/src/renderer/views/Watch/Watch.vue
@@ -23,6 +23,7 @@
:storyboard-src="videoStoryboardSrc"
:format="activeFormat"
:thumbnail="thumbnail"
+ :video-id="videoId"
class="videoPlayer"
:class="{ theatrePlayer: useTheatreMode }"
@ready="checkIfWatched"
diff --git a/static/locales/en-US.yaml b/static/locales/en-US.yaml
index f9edfc4e..1e248e16 100644
--- a/static/locales/en-US.yaml
+++ b/static/locales/en-US.yaml
@@ -280,6 +280,11 @@ Settings:
Region: Region
City: City
Error getting network information. Is your proxy configured properly?: Error getting network information. Is your proxy configured properly?
+ SponsorBlock Settings:
+ SponsorBlock Settings: SponsorBlock Settings
+ Enable SponsorBlock: Enable SponsorBlock
+ 'SponsorBlock API Url (Default is https://sponsor.ajay.app)': SponsorBlock API Url (Default is https://sponsor.ajay.app)
+ Notify when sponsor segment is skipped: Notify when sponsor segment is skipped
About:
#On About page
About: About
@@ -472,6 +477,14 @@ Video:
translated from English: translated from English
# $ is replaced with the number and % with the unit (days, hours, minutes...)
Publicationtemplate: $ % ago
+ Skipped segment: Skipped segment
+ Sponsor Block category:
+ sponsor: sponsor
+ intro: intro
+ outro: outro
+ self-promotion: self-promotion
+ interaction: interaction
+ music offtopic: music offtopic
#& Videos
Videos:
#& Sort By