implement sponsorblock skipping, #build

Changed:
- sponsorblock frontent implementation, #208
- per channel sponsorblock
- improved scheduler input validation
This commit is contained in:
simon 2022-04-10 16:26:47 +07:00
commit fd5de99674
No known key found for this signature in database
GPG Key ID: 2C15AA5E89985DD4
16 changed files with 364 additions and 84 deletions

View File

@ -86,7 +86,8 @@ class VideoApiView(ApiBaseView):
# pylint: disable=unused-argument # pylint: disable=unused-argument
"""get request""" """get request"""
self.get_document(video_id) self.get_document(video_id)
self.process_keys() if self.response.get("data"):
self.process_keys()
return Response(self.response, status=self.status_code) return Response(self.response, status=self.status_code)

View File

@ -97,9 +97,8 @@ class StartupCheck:
if invalid: if invalid:
print( print(
"minial required elasticsearch version: " "required elasticsearch version: "
+ f"{self.MIN_MAJOR}.{self.MIN_MINOR}, " + f"{self.MIN_MAJOR}.{self.MIN_MINOR}"
+ "please update to recommended version."
) )
sys.exit(1) sys.exit(1)

View File

@ -4,8 +4,10 @@ functionality:
- check for missing thumbnails - check for missing thumbnails
""" """
import base64
import os import os
from collections import Counter from collections import Counter
from io import BytesIO
from time import sleep from time import sleep
import requests import requests
@ -15,7 +17,7 @@ from home.src.ta.config import AppConfig
from home.src.ta.helper import ignore_filelist from home.src.ta.helper import ignore_filelist
from home.src.ta.ta_redis import RedisArchivist from home.src.ta.ta_redis import RedisArchivist
from mutagen.mp4 import MP4, MP4Cover from mutagen.mp4 import MP4, MP4Cover
from PIL import Image from PIL import Image, ImageFilter
class ThumbManager: class ThumbManager:
@ -241,6 +243,21 @@ class ThumbManager:
} }
RedisArchivist().set_message("message:download", mess_dict) RedisArchivist().set_message("message:download", mess_dict)
def get_base64_blur(self, youtube_id):
"""return base64 encoded placeholder"""
img_path = self.vid_thumb_path(youtube_id)
file_path = os.path.join(self.CACHE_DIR, img_path)
img_raw = Image.open(file_path)
img_raw.thumbnail((img_raw.width // 20, img_raw.height // 20))
img_blur = img_raw.filter(ImageFilter.BLUR)
buffer = BytesIO()
img_blur.save(buffer, format="JPEG")
img_data = buffer.getvalue()
img_base64 = base64.b64encode(img_data).decode()
data_url = f"data:image/jpg;base64,{img_base64}"
return data_url
@staticmethod @staticmethod
def vid_thumb_path(youtube_id): def vid_thumb_path(youtube_id):
"""build expected path for video thumbnail from youtube_id""" """build expected path for video thumbnail from youtube_id"""

View File

@ -177,7 +177,9 @@ class VideoDownloader:
except yt_dlp.utils.DownloadError: except yt_dlp.utils.DownloadError:
print("failed to download " + youtube_id) print("failed to download " + youtube_id)
continue continue
vid_dict = index_new_video(youtube_id) vid_dict = index_new_video(
youtube_id, video_overwrites=self.video_overwrites
)
self.channels.add(vid_dict["channel"]["channel_id"]) self.channels.add(vid_dict["channel"]["channel_id"])
self.move_to_archive(vid_dict) self.move_to_archive(vid_dict)
self._delete_from_pending(youtube_id) self._delete_from_pending(youtube_id)

View File

@ -50,6 +50,9 @@
}, },
"index_playlists": { "index_playlists": {
"type": "boolean" "type": "boolean"
},
"integrate_sponsorblock": {
"type" : "boolean"
} }
} }
} }
@ -73,6 +76,10 @@
"type": "text", "type": "text",
"index": false "index": false
}, },
"vid_thumb_base64": {
"type": "text",
"index": false
},
"date_downloaded": { "date_downloaded": {
"type": "date" "type": "date"
}, },
@ -126,6 +133,9 @@
}, },
"index_playlists": { "index_playlists": {
"type": "boolean" "type": "boolean"
},
"integrate_sponsorblock": {
"type" : "boolean"
} }
} }
} }

View File

@ -198,8 +198,17 @@ class ChannelOverwriteForm(forms.Form):
("1", "Enable playlist index"), ("1", "Enable playlist index"),
] ]
SP_CHOICES = [
("", "-- change sponsorblock integrations"),
("0", "disable sponsorblock integration"),
("1", "enable sponsorblock integration"),
]
download_format = forms.CharField(label=False, required=False) download_format = forms.CharField(label=False, required=False)
autodelete_days = forms.IntegerField(label=False, required=False) autodelete_days = forms.IntegerField(label=False, required=False)
index_playlists = forms.ChoiceField( index_playlists = forms.ChoiceField(
widget=forms.Select, choices=PLAYLIST_INDEX, required=False widget=forms.Select, choices=PLAYLIST_INDEX, required=False
) )
integrate_sponsorblock = forms.ChoiceField(
widget=forms.Select, choices=SP_CHOICES, required=False
)

View File

@ -340,7 +340,12 @@ class YoutubeChannel(YouTubeItem):
def set_overwrites(self, overwrites): def set_overwrites(self, overwrites):
"""set per channel overwrites""" """set per channel overwrites"""
valid_keys = ["download_format", "autodelete_days", "index_playlists"] valid_keys = [
"download_format",
"autodelete_days",
"index_playlists",
"integrate_sponsorblock",
]
to_write = self.json_data.get("channel_overwrites", {}) to_write = self.json_data.get("channel_overwrites", {})
for key, value in overwrites.items(): for key, value in overwrites.items():

View File

@ -10,6 +10,7 @@ from datetime import datetime
import requests import requests
from django.conf import settings from django.conf import settings
from home.src.download.thumbnails import ThumbManager
from home.src.es.connect import ElasticWrap from home.src.es.connect import ElasticWrap
from home.src.index import channel as ta_channel from home.src.index import channel as ta_channel
from home.src.index.generic import YouTubeItem from home.src.index.generic import YouTubeItem
@ -357,9 +358,10 @@ class YoutubeVideo(YouTubeItem, YoutubeSubtitle):
index_name = "ta_video" index_name = "ta_video"
yt_base = "https://www.youtube.com/watch?v=" yt_base = "https://www.youtube.com/watch?v="
def __init__(self, youtube_id): def __init__(self, youtube_id, video_overwrites=False):
super().__init__(youtube_id) super().__init__(youtube_id)
self.channel_id = False self.channel_id = False
self.video_overwrites = video_overwrites
self.es_path = f"{self.index_name}/_doc/{youtube_id}" self.es_path = f"{self.index_name}/_doc/{youtube_id}"
def build_json(self): def build_json(self):
@ -376,11 +378,24 @@ class YoutubeVideo(YouTubeItem, YoutubeSubtitle):
if self.config["downloads"]["integrate_ryd"]: if self.config["downloads"]["integrate_ryd"]:
self._get_ryd_stats() self._get_ryd_stats()
if self.config["downloads"]["integrate_sponsorblock"]: if self._check_get_sb():
self._get_sponsorblock() self._get_sponsorblock()
return return
def _check_get_sb(self):
"""check if need to run sponsor block"""
if self.config["downloads"]["integrate_sponsorblock"]:
return True
try:
single_overwrite = self.video_overwrites[self.youtube_id]
_ = single_overwrite["integrate_sponsorblock"]
return True
except KeyError:
return False
return False
def _process_youtube_meta(self): def _process_youtube_meta(self):
"""extract relevant fields from youtube""" """extract relevant fields from youtube"""
# extract # extract
@ -389,12 +404,14 @@ class YoutubeVideo(YouTubeItem, YoutubeSubtitle):
upload_date_time = datetime.strptime(upload_date, "%Y%m%d") upload_date_time = datetime.strptime(upload_date, "%Y%m%d")
published = upload_date_time.strftime("%Y-%m-%d") published = upload_date_time.strftime("%Y-%m-%d")
last_refresh = int(datetime.now().strftime("%s")) last_refresh = int(datetime.now().strftime("%s"))
base64_blur = ThumbManager().get_base64_blur(self.youtube_id)
# build json_data basics # build json_data basics
self.json_data = { self.json_data = {
"title": self.youtube_meta["title"], "title": self.youtube_meta["title"],
"description": self.youtube_meta["description"], "description": self.youtube_meta["description"],
"category": self.youtube_meta["categories"], "category": self.youtube_meta["categories"],
"vid_thumb_url": self.youtube_meta["thumbnail"], "vid_thumb_url": self.youtube_meta["thumbnail"],
"vid_thumb_base64": base64_blur,
"tags": self.youtube_meta["tags"], "tags": self.youtube_meta["tags"],
"published": published, "published": published,
"vid_last_refresh": last_refresh, "vid_last_refresh": last_refresh,
@ -495,7 +512,10 @@ class YoutubeVideo(YouTubeItem, YoutubeSubtitle):
for media_url in to_del: for media_url in to_del:
file_path = os.path.join(video_base, media_url) file_path = os.path.join(video_base, media_url)
os.remove(file_path) try:
os.remove(file_path)
except FileNotFoundError:
print(f"{self.youtube_id}: failed {media_url}, continue.")
self.del_in_es() self.del_in_es()
self.delete_subtitles() self.delete_subtitles()
@ -541,9 +561,9 @@ class YoutubeVideo(YouTubeItem, YoutubeSubtitle):
_, _ = ElasticWrap(path).post(data=data) _, _ = ElasticWrap(path).post(data=data)
def index_new_video(youtube_id): def index_new_video(youtube_id, video_overwrites=False):
"""combined classes to create new video in index""" """combined classes to create new video in index"""
video = YoutubeVideo(youtube_id) video = YoutubeVideo(youtube_id, video_overwrites=video_overwrites)
video.build_json() video.build_json()
if not video.json_data: if not video.json_data:
raise ValueError("failed to get metadata for " + youtube_id) raise ValueError("failed to get metadata for " + youtube_id)

View File

@ -83,33 +83,32 @@ class AppConfig:
def update_config(self, form_post): def update_config(self, form_post):
"""update config values from settings form""" """update config values from settings form"""
config = self.config
for key, value in form_post.items(): for key, value in form_post.items():
to_write = value[0] if not value and not isinstance(value, int):
if len(to_write): continue
if to_write == "0":
to_write = False
elif to_write == "1":
to_write = True
elif to_write.isdigit():
to_write = int(to_write)
config_dict, config_value = key.split("_", maxsplit=1) if value in ["0", 0]:
config[config_dict][config_value] = to_write to_write = False
elif value == "1":
to_write = True
else:
to_write = value
RedisArchivist().set_message("config", config, expire=False) config_dict, config_value = key.split("_", maxsplit=1)
self.config[config_dict][config_value] = to_write
RedisArchivist().set_message("config", self.config, expire=False)
@staticmethod @staticmethod
def set_user_config(form_post, user_id): def set_user_config(form_post, user_id):
"""set values in redis for user settings""" """set values in redis for user settings"""
for key, value in form_post.items(): for key, value in form_post.items():
to_write = value[0] if not value:
if len(to_write): continue
if to_write.isdigit():
to_write = int(to_write) message = {"status": value}
message = {"status": to_write} redis_key = f"{user_id}:{key}"
redis_key = f"{user_id}:{key}" RedisArchivist().set_message(redis_key, message, expire=False)
RedisArchivist().set_message(redis_key, message, expire=False)
def get_colors(self): def get_colors(self):
"""overwrite config if user has set custom values""" """overwrite config if user has set custom values"""
@ -172,12 +171,11 @@ class ScheduleBuilder:
print("processing form, restart container for changes to take effect") print("processing form, restart container for changes to take effect")
redis_config = self.config redis_config = self.config
for key, value in form_post.items(): for key, value in form_post.items():
to_check = value[0] if key in self.SCHEDULES and value:
if key in self.SCHEDULES and to_check:
try: try:
to_write = self.value_builder(key, to_check) to_write = self.value_builder(key, value)
except ValueError: except ValueError:
print(f"failed: {key} {to_check}") print(f"failed: {key} {value}")
mess_dict = { mess_dict = {
"status": "message:setting", "status": "message:setting",
"level": "error", "level": "error",
@ -188,8 +186,8 @@ class ScheduleBuilder:
return return
redis_config["scheduler"][key] = to_write redis_config["scheduler"][key] = to_write
if key in self.CONFIG and to_check: if key in self.CONFIG and value:
redis_config["scheduler"][key] = int(to_check) redis_config["scheduler"][key] = int(value)
RedisArchivist().set_message("config", redis_config, expire=False) RedisArchivist().set_message("config", redis_config, expire=False)
mess_dict = { mess_dict = {
"status": "message:setting", "status": "message:setting",
@ -199,37 +197,56 @@ class ScheduleBuilder:
} }
RedisArchivist().set_message("message:setting", mess_dict) RedisArchivist().set_message("message:setting", mess_dict)
def value_builder(self, key, to_check): def value_builder(self, key, value):
"""validate single cron form entry and return cron dict""" """validate single cron form entry and return cron dict"""
print(f"change schedule for {key} to {to_check}") print(f"change schedule for {key} to {value}")
if to_check == "0": if value == "0":
# deactivate this schedule # deactivate this schedule
return False return False
if re.search(r"[\d]{1,2}\/[\d]{1,2}", to_check): if re.search(r"[\d]{1,2}\/[\d]{1,2}", value):
# number/number cron format will fail in celery # number/number cron format will fail in celery
print("number/number schedule formatting not supported") print("number/number schedule formatting not supported")
raise ValueError raise ValueError
keys = ["minute", "hour", "day_of_week"] keys = ["minute", "hour", "day_of_week"]
if to_check == "auto": if value == "auto":
# set to sensible default # set to sensible default
values = self.SCHEDULES[key].split() values = self.SCHEDULES[key].split()
else: else:
values = to_check.split() values = value.split()
if len(keys) != len(values): if len(keys) != len(values):
print(f"failed to parse {to_check} for {key}") print(f"failed to parse {value} for {key}")
raise ValueError("invalid input") raise ValueError("invalid input")
to_write = dict(zip(keys, values)) to_write = dict(zip(keys, values))
try: self._validate_cron(to_write)
int(to_write["minute"])
except ValueError as error:
print("too frequent: only number in minutes are supported")
raise ValueError("invalid input") from error
return to_write return to_write
@staticmethod
def _validate_cron(to_write):
"""validate all fields, raise value error for impossible schedule"""
all_hours = list(re.split(r"\D+", to_write["hour"]))
for hour in all_hours:
if hour.isdigit() and int(hour) > 23:
print("hour can not be greater than 23")
raise ValueError("invalid input")
all_days = list(re.split(r"\D+", to_write["day_of_week"]))
for day in all_days:
if day.isdigit() and int(day) > 6:
print("day can not be greater than 6")
raise ValueError("invalid input")
if not to_write["minute"].isdigit():
print("too frequent: only number in minutes are supported")
raise ValueError("invalid input")
if int(to_write["minute"]) > 59:
print("minutes can not be greater than 59")
raise ValueError("invalid input")
def build_schedule(self): def build_schedule(self):
"""build schedule dict as expected by app.conf.beat_schedule""" """build schedule dict as expected by app.conf.beat_schedule"""
schedule_dict = {} schedule_dict = {}

View File

@ -89,7 +89,15 @@
{% endif %}</span></p> {% endif %}</span></p>
{{ channel_overwrite_form.index_playlists }}<br> {{ channel_overwrite_form.index_playlists }}<br>
</div> </div>
<div class="overwrite-form-item"></div> <div class="overwrite-form-item">
<p>Enable <a href="https://sponsor.ajay.app/" target="_blank">SponsorBlock</a>: <span class="settings-current">
{% if channel_info.channel_overwrites.integrate_sponsorblock %}
{{ channel_info.channel_overwrites.integrate_sponsorblock }}
{% else %}
False
{% endif %}</span></p>
{{ channel_overwrite_form.integrate_sponsorblock }}<br>
</div>
<button type="submit">Save Channel Overwrites</button> <button type="submit">Save Channel Overwrites</button>
</form> </form>
</div> </div>

View File

@ -124,12 +124,12 @@
</div> </div>
</div> </div>
<div class="settings-item"> <div class="settings-item">
<p>Integrate with <a href="https://returnyoutubedislike.com/">returnyoutubedislike.com</a> to get dislikes and average ratings back: <span class="settings-current">{{ config.downloads.integrate_ryd }}</span></p> <p>Integrate with <a href="https://returnyoutubedislike.com/" target="_blank">returnyoutubedislike.com</a> to get dislikes and average ratings back: <span class="settings-current">{{ config.downloads.integrate_ryd }}</span></p>
<i>Before activating that, make sure you have a scraping sleep interval of at least 3 secs set to avoid ratelimiting issues.</i><br> <i>Before activating that, make sure you have a scraping sleep interval of at least 3 secs set to avoid ratelimiting issues.</i><br>
{{ app_form.downloads_integrate_ryd }} {{ app_form.downloads_integrate_ryd }}
</div> </div>
<div class="settings-item"> <div class="settings-item">
<p>Integrate with <a href="https://sponsor.ajay.app/">SponsorBlock</a> to get sponsored timestamps: <span class="settings-current">{{ config.downloads.integrate_sponsorblock }}</span></p> <p>Integrate with <a href="https://sponsor.ajay.app/" target="_blank">SponsorBlock</a> to get sponsored timestamps: <span class="settings-current">{{ config.downloads.integrate_sponsorblock }}</span></p>
<i>Before activating that, make sure you have a scraping sleep interval of at least 3 secs set to avoid ratelimiting issues.</i><br> <i>Before activating that, make sure you have a scraping sleep interval of at least 3 secs set to avoid ratelimiting issues.</i><br>
{{ app_form.downloads_integrate_sponsorblock }} {{ app_form.downloads_integrate_sponsorblock }}
</div> </div>

View File

@ -3,6 +3,36 @@
{% load static %} {% load static %}
{% load humanize %} {% load humanize %}
<div class="video-main"></div> <div class="video-main"></div>
<div class="notifications" id="notifications"></div>
<div class="sponsorblock" id="sponsorblock">
{% if video.channel.channel_overwrites.integrate_sponsorblock %}
{% if video.channel.channel_overwrites.integrate_sponsorblock == True %}
{% if not video.sponsorblock %}
<h4>This video doesn't have any sponsor segments added. To add a segment go to <u><a href="https://www.youtube.com/watch?v={{ video.youtube_id }}">this video on YouTube</a></u> and add a segment using the <u><a href="https://sponsor.ajay.app/">SponsorBlock</a></u> extension.</h4>
{% endif %}
{% if video.sponsorblock %}
{% for segment in video.sponsorblock %}
{% if segment.locked != 1 %}
<h4>This video has unlocked sponsor segments. Go to <u><a href="https://www.youtube.com/watch?v={{ video.youtube_id }}">this video on YouTube</a></u> and vote on the segments using the <u><a href="https://sponsor.ajay.app/">SponsorBlock</a></u> extension.</h4>
{{ break }}
{% endif %}
{% endfor %}
{% endif %}
{% endif %}
{% elif config.downloads.integrate_sponsorblock %}
{% if not video.sponsorblock %}
<h4>This video doesn't have any sponsor segments added. To add a segment go to <u><a href="https://www.youtube.com/watch?v={{ video.youtube_id }}">this video on YouTube</a></u> and add a segment using the <u><a href="https://sponsor.ajay.app/">SponsorBlock</a></u> extension.</h4>
{% endif %}
{% if video.sponsorblock %}
{% for segment in video.sponsorblock %}
{% if segment.locked != 1 %}
<h4>This video has unlocked sponsor segments. Go to <u><a href="https://www.youtube.com/watch?v={{ video.youtube_id }}">this video on YouTube</a></u> and vote on the segments using the <u><a href="https://sponsor.ajay.app/">SponsorBlock</a></u> extension.</h4>
{{ break }}
{% endif %}
{% endfor %}
{% endif %}
{% endif %}
</div>
<div class="boxed-content"> <div class="boxed-content">
<div class="title-bar"> <div class="title-bar">
{% if cast %} {% if cast %}
@ -114,6 +144,7 @@
</div> </div>
<script> <script>
var videoData = getVideoData('{{ video.youtube_id }}'); var videoData = getVideoData('{{ video.youtube_id }}');
sponsorBlock = videoData.data.sponsorblock;
var videoProgress = getVideoProgress('{{ video.youtube_id }}').position; var videoProgress = getVideoProgress('{{ video.youtube_id }}').position;
window.onload = insertVideoTag(videoData, videoProgress); window.onload = insertVideoTag(videoData, videoProgress);
</script> </script>

View File

@ -8,7 +8,6 @@ import json
import urllib.parse import urllib.parse
from time import sleep from time import sleep
from django import forms
from django.conf import settings from django.conf import settings
from django.contrib.auth import login from django.contrib.auth import login
from django.contrib.auth.forms import AuthenticationForm from django.contrib.auth.forms import AuthenticationForm
@ -112,7 +111,7 @@ class ArchivistViewConfig(View):
"""build default context for every view""" """build default context for every view"""
self.user_id = user_id self.user_id = user_id
self.user_conf = RedisArchivist() self.user_conf = RedisArchivist()
self.default_conf = AppConfig().config self.default_conf = AppConfig(self.user_id).config
self.context = { self.context = {
"colors": self.default_conf["application"]["colors"], "colors": self.default_conf["application"]["colors"],
@ -671,7 +670,7 @@ class VideoView(View):
def get(self, request, video_id): def get(self, request, video_id):
"""get single video""" """get single video"""
colors, cast = self.read_config(user_id=request.user.id) config_handler = AppConfig(request.user.id)
path = f"ta_video/_doc/{video_id}" path = f"ta_video/_doc/{video_id}"
look_up = SearchHandler(path, config=False) look_up = SearchHandler(path, config=False)
video_hit = look_up.get_data() video_hit = look_up.get_data()
@ -693,9 +692,10 @@ class VideoView(View):
"video": video_data, "video": video_data,
"playlist_nav": playlist_nav, "playlist_nav": playlist_nav,
"title": video_title, "title": video_title,
"colors": colors, "colors": config_handler.colors,
"cast": cast, "cast": config_handler.config["application"]["enable_cast"],
"version": settings.TA_VERSION, "version": settings.TA_VERSION,
"config": config_handler.config,
} }
return render(request, "home/video.html", context) return render(request, "home/video.html", context)
@ -712,14 +712,6 @@ class VideoView(View):
return all_navs return all_navs
@staticmethod
def read_config(user_id):
"""read config file"""
config_handler = AppConfig(user_id)
cast = config_handler.config["application"]["enable_cast"]
colors = config_handler.colors
return colors, cast
@staticmethod @staticmethod
def star_creator(rating): def star_creator(rating):
"""convert rating float to stars""" """convert rating float to stars"""
@ -802,23 +794,25 @@ class SettingsView(View):
@staticmethod @staticmethod
def post(request): def post(request):
"""handle form post to update settings""" """handle form post to update settings"""
user_form = UserSettingsForm(request.POST)
if user_form.is_valid():
user_form_post = user_form.cleaned_data
if any(user_form_post.values()):
AppConfig().set_user_config(user_form_post, request.user.id)
form_response = forms.Form(request.POST) app_form = ApplicationSettingsForm(request.POST)
if form_response.is_valid(): if app_form.is_valid():
form_post = dict(request.POST) app_form_post = app_form.cleaned_data
print(form_post) if app_form_post:
del form_post["csrfmiddlewaretoken"] print(app_form_post)
config_handler = AppConfig() AppConfig().update_config(app_form_post)
if "application-settings" in form_post:
del form_post["application-settings"] scheduler_form = SchedulerSettingsForm(request.POST)
config_handler.update_config(form_post) if scheduler_form.is_valid():
elif "user-settings" in form_post: scheduler_form_post = scheduler_form.cleaned_data
del form_post["user-settings"] if any(scheduler_form_post.values()):
config_handler.set_user_config(form_post, request.user.id) print(scheduler_form_post)
elif "scheduler-settings" in form_post: ScheduleBuilder().update_schedule_conf(scheduler_form_post)
del form_post["scheduler-settings"]
print(form_post)
ScheduleBuilder().update_schedule_conf(form_post)
sleep(1) sleep(1)
return redirect("settings", permanent=True) return redirect("settings", permanent=True)

View File

@ -1,4 +1,4 @@
beautifulsoup4==4.10.0 beautifulsoup4==4.11.1
celery==5.2.6 celery==5.2.6
Django==4.0.3 Django==4.0.3
django-cors-headers==3.11.0 django-cors-headers==3.11.0
@ -9,4 +9,4 @@ requests==2.27.1
ryd-client==0.0.3 ryd-client==0.0.3
uWSGI==2.0.20 uWSGI==2.0.20
whitenoise==6.0.0 whitenoise==6.0.0
yt_dlp==2022.3.8.2 yt_dlp==2022.4.8

View File

@ -62,6 +62,13 @@ h3 {
color: var(--accent-font-light); color: var(--accent-font-light);
} }
h4 {
font-size: 0.7em;
margin-bottom: 7px;
font-family: Sen-Regular, sans-serif;
color: var(--accent-font-light);
}
p, i, li { p, i, li {
font-family: Sen-Regular, sans-serif; font-family: Sen-Regular, sans-serif;
margin-bottom: 10px; margin-bottom: 10px;
@ -355,6 +362,18 @@ button:hover {
height: 100vh; height: 100vh;
} }
.notifications {
text-align: center;
width: 80%;
margin: auto;
}
.sponsorblock {
text-align: center;
width: 80%;
margin: auto;
}
.video-player video, .video-player video,
.video-main video { .video-main video {
max-height: 80vh; max-height: 80vh;

View File

@ -327,9 +327,33 @@ function cancelDelete() {
} }
// player // player
var sponsorBlock = [];
function createPlayer(button) { function createPlayer(button) {
var videoId = button.getAttribute('data-id'); var videoId = button.getAttribute('data-id');
var videoData = getVideoData(videoId); var videoData = getVideoData(videoId);
var sponsorBlockElements = '';
if (videoData.config.downloads.integrate_sponsorblock && (typeof(videoData.data.channel.channel_overwrites) == "undefined" || typeof(videoData.data.channel.channel_overwrites.integrate_sponsorblock) == "undefined" || videoData.data.channel.channel_overwrites.integrate_sponsorblock == true)) {
sponsorBlock = videoData.data.sponsorblock;
if (!sponsorBlock) {
sponsorBlockElements = `
<div class="sponsorblock" id="sponsorblock">
<h4>This video doesn't have any sponsor segments added. To add a segment go to <u><a href="https://www.youtube.com/watch?v=${videoId}">this video on Youtube</a></u> and add a segment using the <u><a href="https://sponsor.ajay.app/">SponsorBlock</a></u> extension.</h4>
</div>
`;
} else {
for(let i in sponsorBlock) {
if(sponsorBlock[i].locked != 1) {
sponsorBlockElements = `
<div class="sponsorblock" id="sponsorblock">
<h4>This video has unlocked sponsor segments. Go to <u><a href="https://www.youtube.com/watch?v=${videoId}">this video on YouTube</a></u> and vote on the segments using the <u><a href="https://sponsor.ajay.app/">SponsorBlock</a></u> extension.</h4>
</div>
`;
break;
}
}
}
}
var videoProgress = getVideoProgress(videoId).position; var videoProgress = getVideoProgress(videoId).position;
var videoName = videoData.data.title; var videoName = videoData.data.title;
@ -353,7 +377,6 @@ function createPlayer(button) {
var channelName = videoData.data.channel.channel_name; var channelName = videoData.data.channel.channel_name;
removePlayer(); removePlayer();
// document.getElementById(videoId).outerHTML = ''; // Remove watch indicator from video info
// If cast integration is enabled create cast button // If cast integration is enabled create cast button
var castButton = ''; var castButton = '';
@ -383,6 +406,8 @@ function createPlayer(button) {
const markup = ` const markup = `
<div class="video-player" data-id="${videoId}"> <div class="video-player" data-id="${videoId}">
${videoTag} ${videoTag}
<div class="notifications" id="notifications"></div>
${sponsorBlockElements}
<div class="player-title boxed-content"> <div class="player-title boxed-content">
<img class="close-button" src="/static/img/icon-close.svg" alt="close-icon" data="${videoId}" onclick="removePlayer()" title="Close player"> <img class="close-button" src="/static/img/icon-close.svg" alt="close-icon" data="${videoId}" onclick="removePlayer()" title="Close player">
${watchStatusIndicator} ${watchStatusIndicator}
@ -400,6 +425,53 @@ function createPlayer(button) {
divPlayer.innerHTML = markup; divPlayer.innerHTML = markup;
} }
// function sendSponsorBlockVote(uuid, vote) {
// var videoId = getVideoPlayerVideoId();
// postSponsorSegmentVote(videoId, uuid, vote);
// }
// var sponsorBlockTimestamps = [];
// function sendSponsorBlockSegment() {
// var videoId = getVideoPlayerVideoId();
// var currentTime = getVideoPlayerCurrentTime();
// var sponsorBlockElement = document.getElementById("sponsorblock");
// if (sponsorBlockTimestamps[1]) {
// if (sponsorBlockTimestamps[1] > sponsorBlockTimestamps[0]) {
// postSponsorSegment(videoId, sponsorBlockTimestamps[0], sponsorBlockTimestamps[1]);
// sponsorBlockElement.innerHTML = `
// <p>Timestamps sent! (Not really)</p>
// `;
// setTimeout(function(){
// sponsorBlockElement.innerHTML = `
// <img src="/static/img/PlayerStartIconSponsorBlocker.svg" class="sponsorblockIcon"><button onclick="sendSponsorBlockSegment()">Start</button>
// `;
// }, 3000);
// } else {
// sponsorBlockElement.innerHTML = `
// <span class="danger-zone">Invalid Timestamps!</span>
// `;
// setTimeout(function(){
// sponsorBlockElement.innerHTML = `
// <img src="/static/img/PlayerStartIconSponsorBlocker.svg" class="sponsorblockIcon"><button onclick="sendSponsorBlockSegment()">Start</button>
// `;
// }, 3000);
// }
// sponsorBlockTimestamps = [];
// } else if (sponsorBlockTimestamps[0]) {
// sponsorBlockTimestamps.push(currentTime);
// sponsorBlockElement.innerHTML = `
// <img src="/static/img/PlayerStartIconSponsorBlocker.svg" class="sponsorblockIcon" onclick="getVideoPlayer().currentTime = '${sponsorBlockTimestamps[0]}'"><p>${sponsorBlockTimestamps[0].toFixed(1)} s | </p>
// <img src="/static/img/PlayerStopIconSponsorBlocker.svg" class="sponsorblockIcon" onclick="getVideoPlayer().currentTime = '${sponsorBlockTimestamps[1]}'"><p>${sponsorBlockTimestamps[1].toFixed(1)} s | </p>
// <img src="/static/img/PlayerUploadIconSponsorBlocker.svg" class="sponsorblockIcon"><button onclick="sendSponsorBlockSegment()">Confirm</button>
// `;
// } else {
// sponsorBlockTimestamps.push(currentTime);
// sponsorBlockElement.innerHTML = `
// <img src="/static/img/PlayerStopIconSponsorBlocker.svg" class="sponsorblockIcon"><button onclick="sendSponsorBlockSegment()">End</button>
// `;
// }
// }
// Add video tag to video page when passed a video id, function loaded on page load `video.html (115-117)` // Add video tag to video page when passed a video id, function loaded on page load `video.html (115-117)`
function insertVideoTag(videoData, videoProgress) { function insertVideoTag(videoData, videoProgress) {
var videoTag = createVideoTag(videoData, videoProgress); var videoTag = createVideoTag(videoData, videoProgress);
@ -488,6 +560,32 @@ function onVideoProgress() {
var videoId = getVideoPlayerVideoId(); var videoId = getVideoPlayerVideoId();
var currentTime = getVideoPlayerCurrentTime(); var currentTime = getVideoPlayerCurrentTime();
var duration = getVideoPlayerDuration(); var duration = getVideoPlayerDuration();
var videoElement = getVideoPlayer();
// var sponsorBlockElement = document.getElementById("sponsorblock");
var notificationsElement = document.getElementById("notifications");
if (sponsorBlock) {
for(let i in sponsorBlock) {
if(sponsorBlock[i].segment[0] <= currentTime + 0.3 && sponsorBlock[i].segment[0] >= currentTime) {
videoElement.currentTime = sponsorBlock[i].segment[1];
notificationsElement.innerHTML += `<h3 id="notification-${sponsorBlock[i].UUID}">Skipped sponsor segment from ${formatTime(sponsorBlock[i].segment[0])} to ${formatTime(sponsorBlock[i].segment[1])}.</h3>`;
}
// if(currentTime >= sponsorBlock[i].segment[1] && currentTime <= sponsorBlock[i].segment[1] + 0.2) {
// if(sponsorBlock[i].locked != 1) {
// sponsorBlockElement.innerHTML += `
// <div id="${sponsorBlock[i].UUID}">
// <button onclick="sendSponsorBlockVote('${sponsorBlock[i].UUID}', 1)">Up Vote</button>
// <button onclick="sendSponsorBlockVote('${sponsorBlock[i].UUID}', -1)">Down Vote</button>
// </div>`;
// }
// }
if(currentTime > sponsorBlock[i].segment[1] + 10) {
var notificationsElementUUID = document.getElementById("notification-" + sponsorBlock[i].UUID);
if(notificationsElementUUID) {
notificationsElementUUID.outerHTML = '';
}
}
}
}
if ((currentTime % 10).toFixed(1) <= 0.2) { // Check progress every 10 seconds or else progress is checked a few times a second if ((currentTime % 10).toFixed(1) <= 0.2) { // Check progress every 10 seconds or else progress is checked a few times a second
postVideoProgress(videoId, currentTime); postVideoProgress(videoId, currentTime);
if (!getVideoPlayerWatchStatus()) { // Check if video is already marked as watched if (!getVideoPlayerWatchStatus()) { // Check if video is already marked as watched
@ -542,6 +640,32 @@ function formatNumbers(number) {
return numberFormatted; return numberFormatted;
} }
// Formats times in seconds for frontend
function formatTime(time) {
var hoursUnformatted = time / 3600;
var minutesUnformatted = (time % 3600) / 60;
var secondsUnformatted = time % 60;
var hoursFormatted = Math.trunc(hoursUnformatted);
if(minutesUnformatted < 10 && hoursFormatted > 0) {
var minutesFormatted = "0" + Math.trunc(minutesUnformatted);
} else {
var minutesFormatted = Math.trunc(minutesUnformatted);
}
if(secondsUnformatted < 10) {
var secondsFormatted = "0" + Math.trunc(secondsUnformatted);
} else {
var secondsFormatted = Math.trunc(secondsUnformatted);
}
var timeUnformatted = '';
if(hoursFormatted > 0) {
timeUnformatted = hoursFormatted + ":"
}
var timeFormatted = timeUnformatted.concat(minutesFormatted, ":", secondsFormatted);
return timeFormatted;
}
// Gets video data when passed video ID // Gets video data when passed video ID
function getVideoData(videoId) { function getVideoData(videoId) {
var apiEndpoint = "/api/video/" + videoId + "/"; var apiEndpoint = "/api/video/" + videoId + "/";
@ -599,6 +723,30 @@ function postVideoProgress(videoId, videoProgress) {
} }
} }
// Send sponsor segment when given video id and and timestamps
function postSponsorSegment(videoId, startTime, endTime) {
var apiEndpoint = "/api/video/" + videoId + "/sponsor/";
var data = {
"segment": {
"startTime": startTime,
"endTime": endTime
}
};
apiRequest(apiEndpoint, "POST", data);
}
// Send sponsor segment when given video id and and timestamps
function postSponsorSegmentVote(videoId, uuid, vote) {
var apiEndpoint = "/api/video/" + videoId + "/sponsor/";
var data = {
"vote": {
"uuid": uuid,
"yourVote": vote
}
};
apiRequest(apiEndpoint, "POST", data);
}
// Makes api requests when passed an endpoint and method ("GET", "POST", "DELETE") // Makes api requests when passed an endpoint and method ("GET", "POST", "DELETE")
function apiRequest(apiEndpoint, method, data) { function apiRequest(apiEndpoint, method, data) {
const xhttp = new XMLHttpRequest(); const xhttp = new XMLHttpRequest();