From 440b04bbf06b10e7090c1bb764073759be405e65 Mon Sep 17 00:00:00 2001 From: Filip Czaplicki Date: Sun, 16 May 2021 22:01:24 +0200 Subject: [PATCH] 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 --- package-lock.json | 3 +- package.json | 1 + .../ft-video-player/ft-video-player.js | 116 ++++++++++++++++++ .../sponsor-block-settings.css | 25 ++++ .../sponsor-block-settings.js | 48 ++++++++ .../sponsor-block-settings.vue | 37 ++++++ src/renderer/store/modules/settings.js | 58 ++++++++- src/renderer/store/modules/sponsorblock.js | 38 ++++++ src/renderer/views/Settings/Settings.js | 4 +- src/renderer/views/Settings/Settings.vue | 1 + src/renderer/views/Watch/Watch.vue | 1 + static/locales/en-US.yaml | 13 ++ 12 files changed, 341 insertions(+), 4 deletions(-) create mode 100644 src/renderer/components/sponsor-block-settings/sponsor-block-settings.css create mode 100644 src/renderer/components/sponsor-block-settings/sponsor-block-settings.js create mode 100644 src/renderer/components/sponsor-block-settings/sponsor-block-settings.vue create mode 100644 src/renderer/store/modules/sponsorblock.js 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 @@ + + +