From 240d6569fa4083b6eed89678eb510fb8c33a355f Mon Sep 17 00:00:00 2001 From: "Brian C. Arnold" Date: Fri, 6 Aug 2021 15:50:11 -0400 Subject: [PATCH 1/3] Added change to make player work on iOS inline. --- src/app/player/player.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/player/player.component.html b/src/app/player/player.component.html index c39afed..99b9727 100644 --- a/src/app/player/player.component.html +++ b/src/app/player/player.component.html @@ -4,7 +4,7 @@
-
From 8b1a1a56e354c81175162388549e79eec200faa5 Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Sun, 8 Aug 2021 05:56:47 -0600 Subject: [PATCH 2/3] Added SponsorBlock support for skipping ads when viewing supported videos Updated default value for subscriptions check interval (new value of 86,400 only existed in the default.json) Text inputs in settings menu are now larger --- backend/appdata/default.json | 3 +- backend/config.js | 5 +- backend/consts.js | 4 + package-lock.json | 8 +- package.json | 1 + src/app/app.module.ts | 4 +- .../skip-ad-button.component.html | 1 + .../skip-ad-button.component.scss | 0 .../skip-ad-button.component.spec.ts | 25 ++++ .../skip-ad-button.component.ts | 107 ++++++++++++++++++ src/app/main/main.component.ts | 3 - src/app/player/player.component.css | 6 + src/app/player/player.component.html | 1 + src/app/player/player.component.ts | 16 +-- src/app/posts.services.ts | 5 + src/app/settings/settings.component.html | 5 +- src/app/settings/settings.component.scss | 3 +- 17 files changed, 177 insertions(+), 20 deletions(-) create mode 100644 src/app/components/skip-ad-button/skip-ad-button.component.html create mode 100644 src/app/components/skip-ad-button/skip-ad-button.component.scss create mode 100644 src/app/components/skip-ad-button/skip-ad-button.component.spec.ts create mode 100644 src/app/components/skip-ad-button/skip-ad-button.component.ts 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 99b9727..04b907b 100644 --- a/src/app/player/player.component.html +++ b/src/app/player/player.component.html @@ -6,6 +6,7 @@ +
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..6cb9031 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 { From 11284cb1b3ca0960c12097ef18944cfbc1af024c Mon Sep 17 00:00:00 2001 From: Isaac Abadi Date: Sun, 8 Aug 2021 06:00:15 -0600 Subject: [PATCH 3/3] Fixed unnecessary (and mispelled) class for settings element --- src/app/settings/settings.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/settings/settings.component.html b/src/app/settings/settings.component.html index 6cb9031..bad9daf 100644 --- a/src/app/settings/settings.component.html +++ b/src/app/settings/settings.component.html @@ -266,7 +266,7 @@
Auto-download Twitch Chat
-
+
Also known as a Client ID. Generating a key is easy!