diff --git a/backend/appdata/default.json b/backend/appdata/default.json index a6bb39c..713a92d 100644 --- a/backend/appdata/default.json +++ b/backend/appdata/default.json @@ -31,7 +31,8 @@ "youtube_API_key": "", "use_twitch_API": false, "twitch_API_key": "", - "twitch_auto_download_chat": false + "twitch_auto_download_chat": false, + "use_sponsorblock_API": false }, "Themes": { "default_theme": "default", diff --git a/backend/config.js b/backend/config.js index ec0935e..c0151d7 100644 --- a/backend/config.js +++ b/backend/config.js @@ -208,7 +208,8 @@ DEFAULT_CONFIG = { "youtube_API_key": "", "use_twitch_API": false, "twitch_API_key": "", - "twitch_auto_download_chat": false + "twitch_auto_download_chat": false, + "use_sponsorblock_API": false }, "Themes": { "default_theme": "default", @@ -217,7 +218,7 @@ DEFAULT_CONFIG = { "Subscriptions": { "allow_subscriptions": true, "subscriptions_base_path": "subscriptions/", - "subscriptions_check_interval": "300", + "subscriptions_check_interval": "86400", "redownload_fresh_uploads": false, "download_delay": "" }, diff --git a/backend/consts.js b/backend/consts.js index fc74b0c..b26eb91 100644 --- a/backend/consts.js +++ b/backend/consts.js @@ -106,6 +106,10 @@ let CONFIG_ITEMS = { 'key': 'ytdl_twitch_auto_download_chat', 'path': 'YoutubeDLMaterial.API.twitch_auto_download_chat' }, + 'ytdl_use_sponsorblock_api': { + 'key': 'ytdl_use_sponsorblock_api', + 'path': 'YoutubeDLMaterial.API.use_sponsorblock_API' + }, // Themes 'ytdl_default_theme': { diff --git a/package-lock.json b/package-lock.json index 61b14b6..0515bca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3800,6 +3800,11 @@ "randomfill": "^1.0.3" } }, + "crypto-js": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz", + "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==" + }, "css": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz", @@ -13743,7 +13748,8 @@ }, "ssri": { "version": "6.0.1", - "resolved": "", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", + "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", "dev": true, "requires": { "figgy-pudding": "^3.5.1" diff --git a/package.json b/package.json index e55369d..a1c0482 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@ngneat/content-loader": "^5.0.0", "@videogular/ngx-videogular": "^2.1.0", "core-js": "^2.4.1", + "crypto-js": "^4.1.1", "file-saver": "^2.0.2", "filesize": "^6.1.0", "fingerprintjs2": "^2.1.0", diff --git a/src/app/app.module.ts b/src/app/app.module.ts index ec638bd..dcaa9ec 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -87,6 +87,7 @@ import { TwitchChatComponent } from './components/twitch-chat/twitch-chat.compon import { LinkifyPipe, SeeMoreComponent } from './components/see-more/see-more.component'; import { H401Interceptor } from './http.interceptor'; import { ConcurrentStreamComponent } from './components/concurrent-stream/concurrent-stream.component'; +import { SkipAdButtonComponent } from './components/skip-ad-button/skip-ad-button.component'; registerLocaleData(es, 'es'); @@ -136,7 +137,8 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible EditCategoryDialogComponent, TwitchChatComponent, SeeMoreComponent, - ConcurrentStreamComponent + ConcurrentStreamComponent, + SkipAdButtonComponent ], imports: [ CommonModule, diff --git a/src/app/components/skip-ad-button/skip-ad-button.component.html b/src/app/components/skip-ad-button/skip-ad-button.component.html new file mode 100644 index 0000000..aeb172e --- /dev/null +++ b/src/app/components/skip-ad-button/skip-ad-button.component.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/app/components/skip-ad-button/skip-ad-button.component.scss b/src/app/components/skip-ad-button/skip-ad-button.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/skip-ad-button/skip-ad-button.component.spec.ts b/src/app/components/skip-ad-button/skip-ad-button.component.spec.ts new file mode 100644 index 0000000..ff8f70a --- /dev/null +++ b/src/app/components/skip-ad-button/skip-ad-button.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SkipAdButtonComponent } from './skip-ad-button.component'; + +describe('SkipAdButtonComponent', () => { + let component: SkipAdButtonComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ SkipAdButtonComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SkipAdButtonComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/skip-ad-button/skip-ad-button.component.ts b/src/app/components/skip-ad-button/skip-ad-button.component.ts new file mode 100644 index 0000000..2222862 --- /dev/null +++ b/src/app/components/skip-ad-button/skip-ad-button.component.ts @@ -0,0 +1,107 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { PostsService } from 'app/posts.services'; +import CryptoJS from 'crypto-js'; + +@Component({ + selector: 'app-skip-ad-button', + templateUrl: './skip-ad-button.component.html', + styleUrls: ['./skip-ad-button.component.scss'] +}) +export class SkipAdButtonComponent implements OnInit { + + @Input() current_video = null; + @Input() playback_timestamp = null; + + @Output() setPlaybackTimestamp = new EventEmitter(); + + sponsor_block_cache = {}; + show_skip_ad_button = false; + + constructor(private postsService: PostsService) { } + + ngOnInit(): void { + setInterval(() => this.skipAdButtonCheck(), 500); + } + + checkSponsorBlock(video_to_check) { + if (!video_to_check) return; + + // check cache, null means it has been checked and confirmed not to exist (limits API calls) + if (this.sponsor_block_cache[video_to_check.url] || this.sponsor_block_cache[video_to_check.url] === null) return; + + // sponsor block needs first 4 chars from video ID hash + const video_id = this.getVideoIDFromURL(video_to_check.url); + const id_hash = this.getVideoIDHashFromURL(video_id); + if (!id_hash || id_hash.length < 4) return; + const truncated_id_hash = id_hash.substring(0, 4); + + // we couldn't get the data from the cache, let's get it from sponsor block directly + + this.postsService.getSponsorBlockDataForVideo(truncated_id_hash).subscribe(res => { + if (res && res['length'] && res['length'] === 0) { + return; + } + + const found_data = res['find'](data => data['videoID'] === video_id); + if (found_data) { + this.sponsor_block_cache[video_to_check.url] = found_data; + } + }, err => { + // likely doesn't exist + this.sponsor_block_cache[video_to_check.url] = null; + }); + } + + getVideoIDHashFromURL(video_id) { + if (!video_id) return null; + return CryptoJS.SHA256(video_id).toString(CryptoJS.enc.Hex);; + } + + getVideoIDFromURL(url) { + const regex_exp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/; + const match = url.match(regex_exp); + return (match && match[7].length==11) ? match[7] : null; + } + + skipAdButtonCheck() { + const sponsor_block_data = this.sponsor_block_cache[this.current_video.url]; + if (!sponsor_block_data && sponsor_block_data !== null) { + // we haven't yet tried to get the sponsor block data for the video + this.checkSponsorBlock(this.current_video); + } else if (!sponsor_block_data) { + this.show_skip_ad_button = false; + return; + } + + if (this.getTimeToSkipTo()) { + this.show_skip_ad_button = true; + } else { + this.show_skip_ad_button = false; + } + } + + getTimeToSkipTo() { + const sponsor_block_data = this.sponsor_block_cache[this.current_video.url]; + + if (!sponsor_block_data) return; + + // check if we're in between an ad segment + const found_segment = sponsor_block_data['segments'].find(segment_data => this.playback_timestamp > segment_data.segment[0] && this.playback_timestamp < segment_data.segment[1] - 0.5); + + if (found_segment) { + return found_segment['segment'][1]; + } + + return null; + } + + skipAdButtonClicked() { + const time_to_skip_to = this.getTimeToSkipTo(); + if (!time_to_skip_to) return; + + this.setPlaybackTimestamp.emit(time_to_skip_to); + + this.show_skip_ad_button = false; + } + +} diff --git a/src/app/main/main.component.ts b/src/app/main/main.component.ts index 8e63fc7..6466e54 100644 --- a/src/app/main/main.component.ts +++ b/src/app/main/main.component.ts @@ -648,7 +648,6 @@ export class MainComponent implements OnInit { return; } this.cachedAvailableFormats[url]['formats'] = this.getAudioAndVideoFormats(infos.formats); - console.log(this.cachedAvailableFormats[url]['formats']); }, err => { this.errorFormats(url); }); @@ -808,8 +807,6 @@ export class MainComponent implements OnInit { const audio_formats: any = {}; const video_formats: any = {}; - console.log(formats); - for (let i = 0; i < formats.length; i++) { const format_obj = {type: null}; diff --git a/src/app/player/player.component.css b/src/app/player/player.component.css index dcd9d4c..4473c96 100644 --- a/src/app/player/player.component.css +++ b/src/app/player/player.component.css @@ -89,4 +89,10 @@ display: inline-block; margin-right: 12px; top: 8px; +} + +.skip-ad-button { + position: absolute; + right: 20px; + bottom: 75px; } \ No newline at end of file diff --git a/src/app/player/player.component.html b/src/app/player/player.component.html index c39afed..04b907b 100644 --- a/src/app/player/player.component.html +++ b/src/app/player/player.component.html @@ -4,8 +4,9 @@
-
diff --git a/src/app/player/player.component.ts b/src/app/player/player.component.ts index e6f6767..bcbb183 100644 --- a/src/app/player/player.component.ts +++ b/src/app/player/player.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, HostListener, EventEmitter, OnDestroy, AfterViewInit, ViewChild, ChangeDetectorRef } from '@angular/core'; +import { Component, OnInit, HostListener, OnDestroy, AfterViewInit, ViewChild, ChangeDetectorRef } from '@angular/core'; import { VgApiService } from '@videogular/ngx-videogular/core'; import { PostsService } from 'app/posts.services'; import { ActivatedRoute, Router } from '@angular/router'; @@ -15,6 +15,7 @@ export interface IMedia { src: string; type: string; label: string; + url: string; } @Component({ @@ -133,7 +134,8 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { title: this.name, label: this.name, src: this.url, - type: 'video/mp4' + type: 'video/mp4', + url: this.url } this.playlist.push(imedia); this.currentItem = this.playlist[0]; @@ -229,7 +231,8 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { title: file_obj['title'], src: fullLocation, type: mime_type, - label: file_obj['title'] + label: file_obj['title'], + url: file_obj['url'] } this.playlist.push(mediaObject); } @@ -289,13 +292,6 @@ export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy { this.currentItem = item; } - getFileInfos() { - const fileNames = this.getFileNames(); - this.postsService.getFileInfo(fileNames, this.type, false).subscribe(res => { - - }); - } - getFileNames() { const fileNames = []; for (let i = 0; i < this.playlist.length; i++) { diff --git a/src/app/posts.services.ts b/src/app/posts.services.ts index 1bfd190..0118ac5 100644 --- a/src/app/posts.services.ts +++ b/src/app/posts.services.ts @@ -614,6 +614,11 @@ export class PostsService implements CanActivate { this.httpOptions); } + getSponsorBlockDataForVideo(id_hash) { + const sponsor_block_api_path = 'https://sponsor.ajay.app/api/'; + return this.http.get(sponsor_block_api_path + `skipSegments/${id_hash}`); + } + public openSnackBar(message: string, action: string = '') { this.snackBar.open(message, action, { duration: 2000, diff --git a/src/app/settings/settings.component.html b/src/app/settings/settings.component.html index c747ec8..bad9daf 100644 --- a/src/app/settings/settings.component.html +++ b/src/app/settings/settings.component.html @@ -266,12 +266,15 @@
Auto-download Twitch Chat
-
+
Also known as a Client ID. Generating a key is easy!
+
+ Use SponsorBlock API +
diff --git a/src/app/settings/settings.component.scss b/src/app/settings/settings.component.scss index f702e64..b59df98 100644 --- a/src/app/settings/settings.component.scss +++ b/src/app/settings/settings.component.scss @@ -32,7 +32,8 @@ } .text-field { - min-width: 30%; + width: 95%; + max-width: 500px; } .checkbox-button {