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
This commit is contained in:
Isaac Abadi
2021-08-08 05:56:47 -06:00
parent 32370280ab
commit 8b1a1a56e3
17 changed files with 177 additions and 20 deletions

View File

@@ -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",

View File

@@ -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": ""
},

View File

@@ -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': {

8
package-lock.json generated
View File

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

View File

@@ -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",

View File

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

View File

@@ -0,0 +1 @@
<button *ngIf="show_skip_ad_button" (click)="skipAdButtonClicked()" mat-flat-button><ng-container i18n="Skip ad button">Skip ad</ng-container></button>

View File

@@ -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<SkipAdButtonComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ SkipAdButtonComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(SkipAdButtonComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -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<any>();
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;
}
}

View File

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

View File

@@ -89,4 +89,10 @@
display: inline-block;
margin-right: 12px;
top: 8px;
}
.skip-ad-button {
position: absolute;
right: 20px;
bottom: 75px;
}

View File

@@ -6,6 +6,7 @@
<vg-player style="height: fit-content; max-height: 75vh" (onPlayerReady)="onPlayerReady($event)" [style.background-color]="(currentItem.type === 'audio/mp3') ? postsService.theme.drawer_color : 'black'">
<video [ngClass]="(currentItem.type === 'audio/mp3') ? 'audio-styles' : 'video-styles'" #media class="video-player" [vgMedia]="media" [src]="currentItem.src" id="singleVideo" preload="auto" controls playsinline>
</video>
<app-skip-ad-button *ngIf="postsService['config']['API']['use_sponsorblock_API'] && api && playlist?.length > 0 && playlist[currentIndex]['type'] === 'video/mp4'" (setPlaybackTimestamp)="setPlaybackTimestamp($event)" [current_video]="playlist[currentIndex]" [playback_timestamp]="api.currentTime" [sponsor_block_cache]="sponsor_block_cache" class="skip-ad-button"></app-skip-ad-button>
</vg-player>
</div>
<div style="height: fit-content; width: 100%; margin-top: 10px;">

View File

@@ -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++) {

View File

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

View File

@@ -266,12 +266,15 @@
<div *ngIf="new_config['API']['use_twitch_API']" class="col-12 mt-1">
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['twitch_auto_download_chat']"><ng-container i18n="Auto download Twitch Chat setting">Auto-download Twitch Chat</ng-container></mat-checkbox>
</div>
<div class="col-12 mb-5">
<div class="col-12 mt=3">
<mat-form-field class="text-field" color="accent">
<input [disabled]="!new_config['API']['use_twitch_API']" [(ngModel)]="new_config['API']['twitch_API_key']" matInput placeholder="Twitch API Key" i18n-placeholder="Twitch API Key setting placeholder" required>
<mat-hint><ng-container i18n="Twitch API Key setting hint AKA preamble">Also known as a Client ID.</ng-container>&nbsp;<a target="_blank" href="https://dev.twitch.tv/docs/api/"><ng-container i18n="Twitch API Key setting hint">Generating a key is easy!</ng-container></a></mat-hint>
</mat-form-field>
</div>
<div class="col-12 mt-4 mb-3">
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['use_sponsorblock_API']" matTooltip="Enables a button to skip ads when viewing supported videos." i18n-matTooltip="SponsorBlock API tooltip"><ng-container i18n="Use SponsorBlock API setting">Use SponsorBlock API</ng-container></mat-checkbox>
</div>
</div>
</div>
<mat-divider></mat-divider>

View File

@@ -32,7 +32,8 @@
}
.text-field {
min-width: 30%;
width: 95%;
max-width: 500px;
}
.checkbox-button {