SponsorBlock (#1130)

* SponsorBlock: enable/url settings

* SponsorBlock: fetch and display skipped fragments

* SponsorBlock: skip sponsor blocks

* npm add node-forge

* SponsorBlock: use hash prefix API

* SponsorBlock: configurable toast on skipped segment

* SponsorBlock: add /api/ to url, remove trailing slash
This commit is contained in:
Filip Czaplicki 2021-05-16 22:01:24 +02:00 committed by GitHub
parent 1096310c9b
commit 440b04bbf0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 341 additions and 4 deletions

3
package-lock.json generated
View File

@ -13739,8 +13739,7 @@
"node-forge": { "node-forge": {
"version": "0.10.0", "version": "0.10.0",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz",
"integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==", "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA=="
"dev": true
}, },
"node-gyp": { "node-gyp": {
"version": "7.1.2", "version": "7.1.2",

View File

@ -28,6 +28,7 @@
"markdown": "^0.5.0", "markdown": "^0.5.0",
"material-design-icons": "^3.0.1", "material-design-icons": "^3.0.1",
"nedb": "^1.8.0", "nedb": "^1.8.0",
"node-forge": "^0.10.0",
"opml-to-json": "^1.0.1", "opml-to-json": "^1.0.1",
"rss-parser": "^3.12.0", "rss-parser": "^3.12.0",
"socks-proxy-agent": "^5.0.0", "socks-proxy-agent": "^5.0.0",

View File

@ -66,6 +66,10 @@ export default Vue.extend({
thumbnail: { thumbnail: {
type: String, type: String,
default: '' default: ''
},
videoId: {
type: String,
required: true
} }
}, },
data: function () { data: function () {
@ -149,6 +153,14 @@ export default Vue.extend({
autoplayVideos: function () { autoplayVideos: function () {
return this.$store.getters.getAutoplayVideos return this.$store.getters.getAutoplayVideos
},
useSponsorBlock: function () {
return this.$store.getters.getUseSponsorBlock
},
sponsorBlockShowSkippedToast: function () {
return this.$store.getters.getSponsorBlockShowSkippedToast
} }
}, },
mounted: function () { 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() { checkAspectRatio() {
@ -1186,6 +1301,7 @@ export default Vue.extend({
}, },
...mapActions([ ...mapActions([
'showToast',
'calculateColorLuminance' 'calculateColorLuminance'
]) ])
} }

View File

@ -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;
}
}

View File

@ -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'
])
}
})

View File

@ -0,0 +1,37 @@
<template>
<ft-card
class="relative card"
>
<h3
class="videoTitle"
>
{{ $t("Settings.SponsorBlock Settings.SponsorBlock Settings") }}
</h3>
<ft-flex-box class="sponsorBlockSettingsFlexBox">
<ft-toggle-switch
:label="$t('Settings.SponsorBlock Settings.Enable SponsorBlock')"
:default-value="useSponsorBlock"
@change="handleUpdateSponsorBlock"
/>
</ft-flex-box>
<ft-flex-box class="sponsorBlockSettingsFlexBox">
<ft-toggle-switch
:label="$t('Settings.SponsorBlock Settings.Notify when sponsor segment is skipped')"
:default-value="sponsorBlockShowSkippedToast"
@change="handleUpdateSponsorBlockShowSkippedToast"
/>
</ft-flex-box>
<ft-flex-box>
<ft-input
:placeholder="$t('Settings.SponsorBlock Settings[\'SponsorBlock API Url (Default is https://sponsor.ajay.app)\']')"
:show-arrow="false"
:show-label="true"
:value="sponsorBlockUrl"
@input="handleUpdateSponsorBlockUrl"
/>
</ft-flex-box>
</ft-card>
</template>
<script src="./sponsor-block-settings.js" />
<style scoped src="./sponsor-block-settings.css" />

View File

@ -78,7 +78,10 @@ const state = {
hidePopularVideos: false, hidePopularVideos: false,
hidePlaylists: false, hidePlaylists: false,
hideLiveChat: false, hideLiveChat: false,
hideActiveSubscriptions: false hideActiveSubscriptions: false,
useSponsorBlock: false,
sponsorBlockUrl: 'https://sponsor.ajay.app',
sponsorBlockShowSkippedToast: true
} }
const getters = { const getters = {
@ -264,6 +267,18 @@ const getters = {
getHideActiveSubscriptions: () => { getHideActiveSubscriptions: () => {
return state.hideActiveSubscriptions return state.hideActiveSubscriptions
},
getUseSponsorBlock: () => {
return state.useSponsorBlock
},
getSponsorBlockUrl: () => {
return state.sponsorBlockUrl
},
getSponsorBlockShowSkippedToast: () => {
return state.sponsorBlockShowSkippedToast
} }
} }
@ -417,6 +432,14 @@ const actions = {
case 'hideActiveSubscriptions': case 'hideActiveSubscriptions':
commit('setHideActiveSubscriptions', result.value) commit('setHideActiveSubscriptions', result.value)
break break
case 'useSponsorBlock':
commit('setUseSponsorBlock', result.value)
break
case 'sponsorBlockUrl':
commit('setSponsorBlockUrl', result.value)
break
case 'sponsorBlockShowSkippedToast':
commit('setSponsorBlockShowSkippedToast', result.value)
} }
}) })
resolve() resolve()
@ -786,6 +809,30 @@ const actions = {
commit('setHideLiveChat', hideLiveChat) 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) { setHideActiveSubscriptions (state, hideActiveSubscriptions) {
state.hideActiveSubscriptions = hideActiveSubscriptions state.hideActiveSubscriptions = hideActiveSubscriptions
},
setUseSponsorBlock (state, useSponsorBlock) {
state.useSponsorBlock = useSponsorBlock
},
setSponsorBlockUrl (state, sponsorBlockUrl) {
state.sponsorBlockUrl = sponsorBlockUrl
},
setSponsorBlockShowSkippedToast (state, sponsorBlockShowSkippedToast) {
state.sponsorBlockShowSkippedToast = sponsorBlockShowSkippedToast
} }
} }

View File

@ -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
}

View File

@ -9,6 +9,7 @@ import PrivacySettings from '../../components/privacy-settings/privacy-settings.
import DataSettings from '../../components/data-settings/data-settings.vue' import DataSettings from '../../components/data-settings/data-settings.vue'
import DistractionSettings from '../../components/distraction-settings/distraction-settings.vue' import DistractionSettings from '../../components/distraction-settings/distraction-settings.vue'
import ProxySettings from '../../components/proxy-settings/proxy-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({ export default Vue.extend({
name: 'Settings', name: 'Settings',
@ -22,6 +23,7 @@ export default Vue.extend({
'privacy-settings': PrivacySettings, 'privacy-settings': PrivacySettings,
'data-settings': DataSettings, 'data-settings': DataSettings,
'distraction-settings': DistractionSettings, 'distraction-settings': DistractionSettings,
'proxy-settings': ProxySettings 'proxy-settings': ProxySettings,
'sponsor-block-settings': SponsorBlockSettings
} }
}) })

View File

@ -8,6 +8,7 @@
<privacy-settings /> <privacy-settings />
<data-settings /> <data-settings />
<proxy-settings /> <proxy-settings />
<sponsor-block-settings />
</div> </div>
</template> </template>

View File

@ -23,6 +23,7 @@
:storyboard-src="videoStoryboardSrc" :storyboard-src="videoStoryboardSrc"
:format="activeFormat" :format="activeFormat"
:thumbnail="thumbnail" :thumbnail="thumbnail"
:video-id="videoId"
class="videoPlayer" class="videoPlayer"
:class="{ theatrePlayer: useTheatreMode }" :class="{ theatrePlayer: useTheatreMode }"
@ready="checkIfWatched" @ready="checkIfWatched"

View File

@ -280,6 +280,11 @@ Settings:
Region: Region Region: Region
City: City City: City
Error getting network information. Is your proxy configured properly?: Error getting network information. Is your proxy configured properly? 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: About:
#On About page #On About page
About: About About: About
@ -472,6 +477,14 @@ Video:
translated from English: translated from English translated from English: translated from English
# $ is replaced with the number and % with the unit (days, hours, minutes...) # $ is replaced with the number and % with the unit (days, hours, minutes...)
Publicationtemplate: $ % ago 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
Videos: Videos:
#& Sort By #& Sort By