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:
		
							parent
							
								
									1096310c9b
								
							
						
					
					
						commit
						440b04bbf0
					
				|  | @ -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", | ||||
|  |  | |||
|  | @ -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", | ||||
|  |  | |||
|  | @ -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' | ||||
|     ]) | ||||
|   } | ||||
|  |  | |||
|  | @ -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; | ||||
|   } | ||||
| } | ||||
|  | @ -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' | ||||
|     ]) | ||||
|   } | ||||
| }) | ||||
|  | @ -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" /> | ||||
|  | @ -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 | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
| } | ||||
|  | @ -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 | ||||
|   } | ||||
| }) | ||||
|  |  | |||
|  | @ -8,6 +8,7 @@ | |||
|     <privacy-settings /> | ||||
|     <data-settings /> | ||||
|     <proxy-settings /> | ||||
|     <sponsor-block-settings /> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
|  |  | |||
|  | @ -23,6 +23,7 @@ | |||
|           :storyboard-src="videoStoryboardSrc" | ||||
|           :format="activeFormat" | ||||
|           :thumbnail="thumbnail" | ||||
|           :video-id="videoId" | ||||
|           class="videoPlayer" | ||||
|           :class="{ theatrePlayer: useTheatreMode }" | ||||
|           @ready="checkIfWatched" | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue