Compare commits

...

23 Commits

Author SHA1 Message Date
simon
6822ed380d Merge video progress bar to master 2022-02-26 09:26:53 +07:00
simon
b28905bbae bump footer version v0.1.2 2022-02-25 12:27:16 +07:00
simon
36a2996cda clean exit of progress builder when no videos in channel 2022-02-25 12:20:22 +07:00
simon
b98ccd9dab handle error if subscribed channel not available anymore, #175 2022-02-25 11:21:29 +07:00
simon
060f0d575e change color of progress bar to dark 2022-02-25 10:51:37 +07:00
Nathan DeTar
99c97a703f Reduce API Calls (#181)
* Reduce API calls

* Fix video id

* Updated `createVideoTag()` description.

* Fixed URL used for cast integration

* Check video duration

* Updates progress bar on watched and close.

* Set progress bar width to 0% by default

* Cleanup, function descriptions

* Cleanup console logging

* Update progress bar on cast progress every 10s

* Catch short <30s videos and mark as watched
2022-02-25 10:39:33 +07:00
simon
806448624d return if no in progress videos 2022-02-24 21:25:12 +07:00
simon
df777104af add video progress bar to channel_id and playlist_id views 2022-02-24 19:30:12 +07:00
simon
d88d6d6a61 add video progress bar 2022-02-24 18:58:26 +07:00
simon
6078d8d276 add youtube_id to progress api key 2022-02-24 18:55:52 +07:00
simon
c4d6bb35a3 add list_items for wildcard matching 2022-02-24 18:55:18 +07:00
simon
a25b101c3a remember video progress, #build
Changes:
- merges video progress API endpoint
- merges fix for auto subscribe reset of playlists
- merges fix for ffmpeg install link builder
2022-02-24 09:43:04 +07:00
simon
e0db73543e Merge branch 'master' into testing 2022-02-24 09:39:56 +07:00
Nathan DeTar
4812b8da55 Save Video Progress (#179)
* Added cast integration docs.

* Changed header sizes.

* Added more space above Requirements

* Added cast integration docs.

* Removed separate cast integration docs

* Further indented quote from Google

* Switch to HTML based video position.

* Ground work for API changes and video progress

* Added onpause attribute to video.

* Added save video progress feature.

* Added API check for subtitle status.

* Switch method to DELETE if position is 0

* Added `createVideoTag()` function

* Added `InsertVideoTag()` function

* Switch to JS generated video tag, add on page load

* Removed extra data from DELETE request

* Removed unused code

* Reduced duplicate code

* Cleanup & groundwork cast pull metadata from API

* Minor bug fix

* Fix saving video progress on player close.

* Only send video progress when unwatched

* Cleanup

* Added `getURL()` function

* Cast use API & save progress/mark as watched

* Added cast progress checks

* Changed thresholds for marking videos as watched

* Added `watchedThreshold()` function
2022-02-24 09:36:31 +07:00
simon
8579fb4cc1 fix ffmpeg link grep by using master 2022-02-21 21:37:49 +07:00
simon
b8b95f9d79 inheirt VideoProgressView from base class to get auth 2022-02-21 20:59:04 +07:00
schizovivek
70506ad8f6 add comment for redis rejson to use arm64 fork created by bbilly1 (#178)
* updated redis rejson to use arm64 fork created by bbilly1

* added arm build as a comment instead of main option for rejson
2022-02-21 20:14:08 +07:00
simon
ec00568008 fix auto reset of playlist_subscribed 2022-02-20 19:22:11 +07:00
simon
2044dba700 API endpoints improvement, #build
- merges adding config key to api return values
- add video progress api endpoint
2022-02-17 18:21:56 +07:00
simon
241d8326f7 add video progress API endpoints 2022-02-17 18:20:30 +07:00
simon
4d83af7c14 bump redis client 2022-02-17 18:19:52 +07:00
simon
265795f4a9 add config key to api response 2022-02-17 16:59:59 +07:00
Nathan DeTar
5c3f0d1e5f Docs for the cast integration (#172)
* Added cast integration docs.

* Changed header sizes.

* Added more space above Requirements

* Added cast integration docs.

* Removed separate cast integration docs

* Further indented quote from Google
2022-02-14 17:10:25 +07:00
19 changed files with 403 additions and 100 deletions

View File

@@ -16,7 +16,7 @@ RUN apt-get clean && apt-get -y update && apt-get -y install --no-install-recomm
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ] ; then \
curl -s https://api.github.com/repos/yt-dlp/FFmpeg-Builds/releases/latest \
| grep browser_download_url \
| grep linux64-gpl-4.4.tar.xz \
| grep ".*master.*linux64.*tar.xz" \
| cut -d '"' -f 4 \
| xargs curl -L --output ffmpeg.tar.xz && \
tar -xf ffmpeg.tar.xz --strip-components=2 --no-anchored -C /usr/bin/ "ffmpeg" && \

View File

@@ -23,7 +23,7 @@ services:
- archivist-es
- archivist-redis
archivist-redis:
image: redislabs/rejson:latest
image: redislabs/rejson:latest # For arm64 just update this line with bbilly1/rejson:latest
container_name: archivist-redis
restart: always
expose:

View File

@@ -36,7 +36,17 @@ Additional settings passed to yt-dlp.
All third party integrations of TubeArchivist will **always** be *opt in*.
- **API**: Your access token for the Tube Archivist API.
- **returnyoutubedislike.com**: This will get return dislikes and average ratings for each video by integrating with the API from [returnyoutubedislike.com](https://www.returnyoutubedislike.com/).
- **Cast**: Enable Google Cast for videos. Requires a valid SSL certificate and works only in Google Chrome.
- **Cast**: Enabling the cast integration in the settings page will load an additional JS library from **Google**.
* Requirements
- HTTPS
* To use the cast integration HTTPS needs to be enabled, which can be done using a reverse proxy. This is a requirement by Google as communication to the cast device is required to be encrypted, but the content itself is not.
- Supported Browser
* A supported browser is required for this integration such as Google Chrome. Other browsers, especially Chromium-based browsers, may support casting by enabling it in the settings.
- Subtitles
* Subtitles are supported however they do not work out of the box and require additional configuration. Due to requirements by Google, to use subtitles you need additional headers which will need to be configured in your reverse proxy. See this [page](https://developers.google.com/cast/docs/web_sender/advanced#cors_requirements) for the specific requirements.
> You need the following headers: Content-Type, Accept-Encoding, and Range. Note that the last two headers, Accept-Encoding and Range, are additional headers that you may not have needed previously.
> Wildcards "*" cannot be used for the Access-Control-Allow-Origin header. If the page has protected media content, it must use a domain instead of a wildcard.
# Scheduler Setup
Schedule settings expect a cron like format, where the first value is minute, second is hour and third is day of the week. Day 0 is Sunday, day 1 is Monday etc.

View File

@@ -23,6 +23,32 @@ response = requests.get(url, headers=headers)
## Video Item View
/api/video/\<video_id>/
## Video Progress View
/api/video/\<video_id>/progress
Progress is stored for each user.
### Get last player position of a video
GET /api/video/\<video_id>/progress
```json
{
"youtube_id": "<video_id>",
"user_id": 1,
"position": 100
}
```
### Post player position of video
POST /api/video/\<video_id>/progress
```json
{
"position": 100
}
```
### Delete player position of video
DELETE /api/video/\<video_id>/progress
## Channel List View
/api/channel/

View File

@@ -7,6 +7,7 @@ from api.views import (
DownloadApiView,
PlaylistApiView,
VideoApiView,
VideoProgressView,
)
from django.urls import path
@@ -16,6 +17,11 @@ urlpatterns = [
VideoApiView.as_view(),
name="api-video",
),
path(
"video/<slug:video_id>/progress/",
VideoProgressView.as_view(),
name="api-video-progress",
),
path(
"channel/",
ChannelApiListView.as_view(),

View File

@@ -4,6 +4,7 @@ import requests
from home.src.download.thumbnails import ThumbManager
from home.src.ta.config import AppConfig
from home.src.ta.helper import UrlListParser
from home.src.ta.ta_redis import RedisArchivist
from home.tasks import extrac_dl, subscribe_to
from rest_framework.authentication import (
SessionAuthentication,
@@ -34,6 +35,7 @@ class ApiBaseView(APIView):
"es_url": self.default_conf["application"]["es_url"],
"es_auth": self.default_conf["application"]["es_auth"],
}
self.response["config"] = self.default_conf
def get_document(self, document_id):
"""get single document from es"""
@@ -98,6 +100,44 @@ class VideoApiView(ApiBaseView):
return Response(self.response, status=self.status_code)
class VideoProgressView(ApiBaseView):
"""resolves to /api/video/<video_id>/
handle progress status for video
"""
def get(self, request, video_id):
"""get progress for a single video"""
user_id = request.user.id
key = f"{user_id}:progress:{video_id}"
video_progress = RedisArchivist().get_message(key)
position = video_progress.get("position", 0)
self.response = {
"youtube_id": video_id,
"user_id": user_id,
"position": position,
}
return Response(self.response)
def post(self, request, video_id):
"""set progress position in redis"""
position = request.data.get("position", 0)
key = f"{request.user.id}:progress:{video_id}"
message = {"position": position, "youtube_id": video_id}
RedisArchivist().set_message(key, message, expire=False)
self.response = request.data
return Response(self.response)
def delete(self, request, video_id):
"""delete progress position"""
key = f"{request.user.id}:progress:{video_id}"
RedisArchivist().del_message(key)
self.response = {"progress-reset": video_id}
return Response(self.response)
class ChannelApiView(ApiBaseView):
"""resolves to /api/channel/<channel_id>/
GET: returns metadata dict of channel

View File

@@ -48,7 +48,13 @@ class ChannelSubscription:
}
if limit:
obs["playlistend"] = self.channel_size
chan = yt_dlp.YoutubeDL(obs).extract_info(url, download=False)
try:
chan = yt_dlp.YoutubeDL(obs).extract_info(url, download=False)
except yt_dlp.utils.DownloadError:
print(f"{channel_id}: failed to extract videos, skipping.")
return False
last_videos = [(i["id"], i["title"]) for i in chan["entries"]]
return last_videos
@@ -66,9 +72,11 @@ class ChannelSubscription:
for idx, channel in enumerate(all_channels):
channel_id = channel["channel_id"]
last_videos = self.get_last_youtube_videos(channel_id)
for video in last_videos:
if video[0] not in to_ignore:
missing_videos.append(video[0])
if last_videos:
for video in last_videos:
if video[0] not in to_ignore:
missing_videos.append(video[0])
# notify
message = {
"status": "message:rescan",

View File

@@ -35,8 +35,11 @@ class YoutubePlaylist(YouTubeItem):
def build_json(self, scrape=False):
"""collection to create json_data"""
if not scrape:
self.get_from_es()
self.get_from_es()
if self.json_data:
subscribed = self.json_data.get("playlist_subscribed")
else:
subscribed = False
if scrape or not self.json_data:
self.get_from_youtube()
@@ -44,13 +47,13 @@ class YoutubePlaylist(YouTubeItem):
self.get_entries()
self.json_data["playlist_entries"] = self.all_members
self.get_playlist_art()
self.json_data["playlist_subscribed"] = subscribed
def process_youtube_meta(self):
"""extract relevant fields from youtube"""
self.json_data = {
"playlist_id": self.youtube_id,
"playlist_active": True,
"playlist_subscribed": False,
"playlist_name": self.youtube_meta["title"],
"playlist_channel": self.youtube_meta["channel"],
"playlist_channel_id": self.youtube_meta["channel_id"],

View File

@@ -59,6 +59,19 @@ class RedisArchivist:
return json_str
def list_items(self, query):
"""list all matches"""
reply = self.redis_connection.execute_command(
"KEYS", self.NAME_SPACE + query + "*"
)
all_matches = [i.decode().lstrip(self.NAME_SPACE) for i in reply]
all_results = []
for match in all_matches:
json_str = self.get_message(match)
all_results.append(json_str)
return all_results
def del_message(self, key):
"""delete key from redis"""
response = self.redis_connection.execute_command(

View File

@@ -132,7 +132,7 @@
</div>
<div class="footer">
<div class="boxed-content">
<span>© 2021 - <script type="text/javascript">document.write(new Date().getFullYear());</script> TubeArchivist v0.1.1 </span><span><a href="{% url 'about' %}">About</a> | <a href="https://github.com/bbilly1/tubearchivist" target="_blank">GitHub</a> | <a href="https://hub.docker.com/r/bbilly1/tubearchivist" target="_blank">Docker Hub</a> | <a href="https://discord.gg/AFwz8nE7BK" target="_blank">Discord</a> | <a href="https://www.reddit.com/r/TubeArchivist/">Reddit</a></span>
<span>© 2021 - <script type="text/javascript">document.write(new Date().getFullYear());</script> TubeArchivist v0.1.2 </span><span><a href="{% url 'about' %}">About</a> | <a href="https://github.com/bbilly1/tubearchivist" target="_blank">GitHub</a> | <a href="https://hub.docker.com/r/bbilly1/tubearchivist" target="_blank">Docker Hub</a> | <a href="https://discord.gg/AFwz8nE7BK" target="_blank">Discord</a> | <a href="https://www.reddit.com/r/TubeArchivist/">Reddit</a></span>
</div>
</div>
</body>

View File

@@ -110,6 +110,11 @@
<div class="video-thumb-wrap {{ view_style }}">
<div class="video-thumb">
<img src="/cache/{{ video.source.vid_thumb_url }}" alt="video-thumb">
{% if video.source.player.progress %}
<div class="video-progress-bar" id="progress-{{ video.source.youtube_id }}" style="width: {{video.source.player.progress}}%;"></div>
{% else %}
<div class="video-progress-bar" id="progress-{{ video.source.youtube_id }}" style="width: 0%;"></div>
{% endif %}
</div>
<div class="video-play">
<img src="{% static 'img/icon-play.svg' %}" alt="play-icon">

View File

@@ -49,6 +49,11 @@
<div class="video-thumb-wrap {{ view_style }}">
<div class="video-thumb">
<img src="/cache/{{ video.source.vid_thumb_url }}" alt="video-thumb">
{% if video.source.player.progress %}
<div class="video-progress-bar" id="progress-{{ video.source.youtube_id }}" style="width: {{video.source.player.progress}}%;"></div>
{% else %}
<div class="video-progress-bar" id="progress-{{ video.source.youtube_id }}" style="width: 0%;"></div>
{% endif %}
</div>
<div class="video-play">
<img src="{% static 'img/icon-play.svg' %}" alt="play-icon">

View File

@@ -91,6 +91,11 @@
<div class="video-thumb-wrap {{ view_style }}">
<div class="video-thumb">
<img src="/cache/{{ video.source.vid_thumb_url }}" alt="video-thumb">
{% if video.source.player.progress %}
<div class="video-progress-bar" id="progress-{{ video.source.youtube_id }}" style="width: {{video.source.player.progress}}%;"></div>
{% else %}
<div class="video-progress-bar" id="progress-{{ video.source.youtube_id }}" style="width: 0%;"></div>
{% endif %}
</div>
<div class="video-play">
<img src="{% static 'img/icon-play.svg' %}" alt="play-icon">

View File

@@ -2,17 +2,7 @@
{% block content %}
{% load static %}
{% load humanize %}
<div class="video-main">
<video poster="/cache/{{ video.vid_thumb_url }}" controls preload="false" width="100%" playsinline
ontimeupdate="onVideoProgress('{{ video.youtube_id }}')" onloadedmetadata="setVideoProgress(0)" id="video-item">
<source src="/media/{{ video.media_url }}" type="video/mp4" id="video-source">
{% if video.subtitles %}
{% for subtitle in video.subtitles %}
<track label="{{subtitle.name}}" kind="subtitles" srclang="{{subtitle.lang}}" src="/media/{{subtitle.media_url}}">
{% endfor %}
{% endif %}
</video>
</div>
<div class="video-main"></div>
<div class="boxed-content">
<div class="title-bar">
{% if cast %}
@@ -122,4 +112,9 @@
{% endfor %}
{% endif %}
</div>
<script>
var videoData = getVideoData('{{ video.youtube_id }}');
var videoProgress = getVideoProgress('{{ video.youtube_id }}').position;
window.onload = insertVideoTag(videoData, videoProgress);
</script>
{% endblock content %}

View File

@@ -169,6 +169,20 @@ class ArchivistResultsView(ArchivistViewConfig):
}
self.data = data
def match_progress(self):
"""add video progress to result context"""
results = RedisArchivist().list_items(f"{self.user_id}:progress:")
if not results or not self.context["results"]:
return
progress = {i["youtube_id"]: i["position"] for i in results}
for hit in self.context["results"]:
video = hit["source"]
if video["youtube_id"] in progress:
played_sec = progress.get(video["youtube_id"])
total = video["player"]["duration"]
video["player"]["progress"] = 100 * (played_sec / total)
def single_lookup(self, es_path):
"""retrieve a single item from url"""
search = SearchHandler(es_path, config=self.default_conf)
@@ -212,6 +226,7 @@ class HomeView(ArchivistResultsView):
self.initiate_vars(request)
self._update_view_data()
self.find_results()
self.match_progress()
return render(request, "home/home.html", self.context)
@@ -355,6 +370,7 @@ class ChannelIdView(ArchivistResultsView):
self.initiate_vars(request)
self._update_view_data(channel_id)
self.find_results()
self.match_progress()
if self.context["results"]:
channel_info = self.context["results"][0]["source"]["channel"]
@@ -456,6 +472,7 @@ class PlaylistIdView(ArchivistResultsView):
playlist_name = playlist_info["playlist_name"]
self._update_view_data(playlist_id, playlist_info)
self.find_results()
self.match_progress()
self.context.update(
{
"title": "Playlist: " + playlist_name,

View File

@@ -4,7 +4,7 @@ Django==4.0.2
django-cors-headers==3.11.0
djangorestframework==3.13.1
Pillow==9.0.1
redis==4.1.3
redis==4.1.4
requests==2.27.1
ryd-client==0.0.3
uWSGI==2.0.20

View File

@@ -13,6 +13,16 @@ function initializeCastApi() {
castConnectionChange(player)
}
);
playerController.addEventListener(
cast.framework.RemotePlayerEventType.CURRENT_TIME_CHANGED, function() {
castVideoProgress(player)
}
);
playerController.addEventListener(
cast.framework.RemotePlayerEventType.IS_PAUSED_CHANGED, function() {
castVideoPaused(player)
}
);
}
@@ -26,32 +36,65 @@ function castConnectionChange(player) {
}
}
function castVideoProgress(player) {
var videoId = getVideoPlayerVideoId();
if (player.mediaInfo.contentId.includes(videoId)) {
var currentTime = player.currentTime;
var duration = player.duration;
if ((currentTime % 10) <= 1.0 && currentTime != 0 && duration != 0) { // Check progress every 10 seconds or else progress is checked a few times a second
postVideoProgress(videoId, currentTime);
setProgressBar(videoId, currentTime, duration);
if (!getVideoPlayerWatchStatus()) { // Check if video is already marked as watched
if (watchedThreshold(currentTime, duration)) {
isWatched(videoId);
}
}
}
}
}
function castVideoPaused(player) {
var videoId = getVideoPlayerVideoId();
var currentTime = player.currentTime;
var duration = player.duration;
if (player.mediaInfo != null) {
if (player.mediaInfo.contentId.includes(videoId)) {
if (currentTime != 0 && duration != 0) {
postVideoProgress(videoId, currentTime);
}
}
}
}
function castStart() {
var castSession = cast.framework.CastContext.getInstance().getCurrentSession();
// Check if there is already media playing on the cast target to prevent recasting on page reload or switching to another video page
if (!castSession.getMediaSession()) {
contentId = document.getElementById("video-source").src; // Get video URL
contentTitle = document.getElementById('video-title').innerHTML; // Get video title
contentImage = document.getElementById("video-item").poster; // Get video thumbnail URL
var videoId = getVideoPlayerVideoId();
var videoData = getVideoData(videoId);
var contentId = getURL() + videoData.data.media_url;
var contentTitle = videoData.data.title;
var contentImage = getURL() + videoData.data.vid_thumb_url;
contentType = 'video/mp4'; // Set content type, only videos right now so it is hard coded
contentCurrentTime = document.getElementById("video-item").currentTime; // Get video's current position
contentCurrentTime = getVideoPlayerCurrentTime(); // Get video's current position
contentActiveSubtitle = [];
// Check if a subtitle is turned on.
for (var i = 0; i < document.getElementById("video-item").textTracks.length; i++) {
if (document.getElementById("video-item").textTracks[i].mode == "showing") {
for (var i = 0; i < getVideoPlayer().textTracks.length; i++) {
if (getVideoPlayer().textTracks[i].mode == "showing") {
contentActiveSubtitle =[i + 1];
}
}
contentSubtitles = [];
for (var i = 0; i < document.getElementById("video-item").children.length; i++) {
if (document.getElementById("video-item").children[i].tagName == "TRACK") {
var videoSubtitles = videoData.data.subtitles; // Array of subtitles
if (typeof(videoSubtitles) != 'undefined' && videoData.config.downloads.subtitle) {
for (var i = 0; i < videoSubtitles.length; i++) {
subtitle = new chrome.cast.media.Track(i, chrome.cast.media.TrackType.TEXT);
subtitle.trackContentId = document.getElementById("video-item").children[i].src;
subtitle.trackContentId = videoSubtitles[i].media_url;
subtitle.trackContentType = 'text/vtt';
subtitle.subtype = chrome.cast.media.TextTrackType.SUBTITLES;
subtitle.name = document.getElementById("video-item").children[i].label;
subtitle.language = document.getElementById("video-item").children[i].srclang;
subtitle.name = videoSubtitles[i].name;
subtitle.language = videoSubtitles[i].lang;
subtitle.customData = null;
contentSubtitles.push(subtitle);
}
@@ -91,7 +134,7 @@ function shiftCurrentTime(contentCurrentTime) { // Shift media back 3 seconds to
function castSuccessful() {
// console.log('Cast Successful.');
document.getElementById("video-item").pause(); // Pause browser video on successful cast
getVideoPlayer().pause(); // Pause browser video on successful cast
}
function castFailed(errorCode) {

View File

@@ -391,8 +391,17 @@ button:hover {
grid-template-columns: 25% auto;
}
.video-progress-bar {
position: absolute;
background-color: var(--accent-font-dark);
height: 7px;
left: 0;
bottom: 3px;
}
.video-thumb img {
width: 100%;
position: relative;
}
.video-play img {

View File

@@ -9,7 +9,8 @@ function sortChange(sortValue) {
}
function isWatched(youtube_id) {
// sendVideoProgress(youtube_id, 0); // Reset video progress on watched;
postVideoProgress(youtube_id, 0); // Reset video progress on watched;
removeProgressBar(youtube_id);
var payload = JSON.stringify({'watched': youtube_id});
sendPost(payload);
var seenIcon = document.createElement('img');
@@ -22,6 +23,11 @@ function isWatched(youtube_id) {
document.getElementById(youtube_id).replaceWith(seenIcon);
}
// Removes the progress bar when passed a video id
function removeProgressBar(videoId) {
setProgressBar(videoId, 0, 1);
}
function isWatchedButton(button) {
youtube_id = button.getAttribute("data-id");
var payload = JSON.stringify({'watched': youtube_id});
@@ -34,7 +40,7 @@ function isWatchedButton(button) {
}
function isUnwatched(youtube_id) {
// sendVideoProgress(youtube_id, 0); // Reset video progress on unwatched;
postVideoProgress(youtube_id, 0); // Reset video progress on unwatched;
var payload = JSON.stringify({'un_watched': youtube_id});
sendPost(payload);
var unseenIcon = document.createElement('img');
@@ -298,20 +304,13 @@ function cancelDelete() {
function createPlayer(button) {
var videoId = button.getAttribute('data-id');
var videoData = getVideoData(videoId);
var videoUrl = videoData.media_url;
var videoThumbUrl = videoData.vid_thumb_url;
var videoName = videoData.title;
var videoProgress = getVideoProgress(videoId).position;
var videoName = videoData.data.title;
var subtitles = '';
var videoSubtitles = videoData.subtitles; // Array of subtitles
if (typeof(videoSubtitles) != 'undefined') {
for (var i = 0; i < videoSubtitles.length; i++) {
subtitles += `<track label="${videoSubtitles[i].name}" kind="subtitles" srclang="${videoSubtitles[i].lang}" src="${videoSubtitles[i].media_url}">`;
}
}
var videoTag = createVideoTag(videoData, videoProgress);
var playlist = '';
var videoPlaylists = videoData.playlist; // Array of playlists the video is in
var videoPlaylists = videoData.data.playlist; // Array of playlists the video is in
if (typeof(videoPlaylists) != 'undefined') {
var subbedPlaylists = getSubbedPlaylists(videoPlaylists); // Array of playlist the video is in that are subscribed
if (subbedPlaylists.length != 0) {
@@ -322,24 +321,22 @@ function createPlayer(button) {
}
}
var videoProgress = videoData.player.progress; // Groundwork for saving video position, change once progress variable is added to API
var videoViews = formatNumbers(videoData.stats.view_count);
var videoViews = formatNumbers(videoData.data.stats.view_count);
var channelId = videoData.channel.channel_id;
var channelName = videoData.channel.channel_name;
var channelId = videoData.data.channel.channel_id;
var channelName = videoData.data.channel.channel_name;
removePlayer();
document.getElementById(videoId).outerHTML = ''; // Remove watch indicator from video info
// If cast integration is enabled create cast button
var castButton = ``;
var castScript = document.getElementById('cast-script');
if (typeof(castScript) != 'undefined' && castScript != null) {
var castButton = '';
if (videoData.config.application.enable_cast) {
var castButton = `<google-cast-launcher id="castbutton"></google-cast-launcher>`;
}
// Watched indicator
if (videoData.player.watched) {
if (videoData.data.player.watched) {
var playerState = "seen";
var watchedFunction = "Unwatched";
} else {
@@ -348,22 +345,19 @@ function createPlayer(button) {
}
var playerStats = `<div class="thumb-icon player-stats"><img src="/static/img/icon-eye.svg" alt="views icon"><span>${videoViews}</span>`;
if (videoData.stats.like_count) {
var likes = formatNumbers(videoData.stats.like_count);
if (videoData.data.stats.like_count) {
var likes = formatNumbers(videoData.data.stats.like_count);
playerStats += `<span>|</span><img src="/static/img/icon-thumb.svg" alt="thumbs-up"><span>${likes}</span>`;
}
if (videoData.stats.dislike_count) {
var dislikes = formatNumbers(videoData.stats.dislike_count);
if (videoData.data.stats.dislike_count && videoData.config.downloads.integrate_ryd) {
var dislikes = formatNumbers(videoData.data.stats.dislike_count);
playerStats += `<span>|</span><img class="dislike" src="/static/img/icon-thumb.svg" alt="thumbs-down"><span>${dislikes}</span>`;
}
playerStats += "</div>";
const markup = `
<div class="video-player" data-id="${videoId}">
<video poster="${videoThumbUrl}" ontimeupdate="onVideoProgress('${videoId}')" controls autoplay width="100%" playsinline id="video-item">
<source src="${videoUrl}#t=${videoProgress}" type="video/mp4" id="video-source">
${subtitles}
</video>
${videoTag}
<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 src="/static/img/icon-${playerState}.svg" alt="${playerState}-icon" id="${videoId}" onclick="is${watchedFunction}(this.id)" class="${playerState}-icon" title="Mark as ${watchedFunction}">
@@ -377,47 +371,127 @@ function createPlayer(button) {
</div>
</div>
`;
const divPlayer = document.getElementById("player");
const divPlayer = document.getElementById("player");
divPlayer.innerHTML = markup;
}
// Set video progress in seconds
function setVideoProgress(videoProgress) {
if (isNaN(videoProgress)) {
videoProgress = 0;
}
var videoElement = document.getElementById("video-item");
videoElement.currentTime = videoProgress;
// Add video tag to video page when passed a video id, function loaded on page load `video.html (115-117)`
function insertVideoTag(videoData, videoProgress) {
var videoTag = createVideoTag(videoData, videoProgress);
var videoMain = document.getElementsByClassName("video-main");
videoMain[0].innerHTML = videoTag;
}
// Runs on video playback, marks video as watched if video gets to 90% or higher, WIP sends position to api
function onVideoProgress(videoId) {
// Generates a video tag with subtitles when passed videoData and videoProgress.
function createVideoTag(videoData, videoProgress) {
var videoId = videoData.data.youtube_id;
var videoUrl = videoData.data.media_url;
var videoThumbUrl = videoData.data.vid_thumb_url;
var subtitles = '';
var videoSubtitles = videoData.data.subtitles; // Array of subtitles
if (typeof(videoSubtitles) != 'undefined' && videoData.config.downloads.subtitle) {
for (var i = 0; i < videoSubtitles.length; i++) {
subtitles += `<track label="${videoSubtitles[i].name}" kind="subtitles" srclang="${videoSubtitles[i].lang}" src="${videoSubtitles[i].media_url}">`;
}
}
var videoTag = `
<video poster="${videoThumbUrl}" ontimeupdate="onVideoProgress()" onpause="onVideoPause()" onended="onVideoEnded()" controls autoplay width="100%" playsinline id="video-item">
<source src="${videoUrl}#t=${videoProgress}" type="video/mp4" id="video-source" videoid="${videoId}">
${subtitles}
</video>
`;
return videoTag;
}
// Gets video tag
function getVideoPlayer() {
var videoElement = document.getElementById("video-item");
return videoElement;
}
// Gets the video source tag
function getVideoPlayerVideoSource() {
var videoPlayerVideoSource = document.getElementById("video-source");
return videoPlayerVideoSource;
}
// Gets the current progress of the video currently in the player
function getVideoPlayerCurrentTime() {
var videoElement = getVideoPlayer();
if (videoElement != null) {
if ((videoElement.currentTime % 10).toFixed(1) <= 0.2) { // Check progress every 10 seconds or else progress is checked a few times a second
// sendVideoProgress(videoId, videoElement.currentTime); // Groundwork for saving video position
if (((videoElement.currentTime / videoElement.duration) >= 0.90) && document.getElementById(videoId).className == "unseen-icon") {
return videoElement.currentTime;
}
}
// Gets the video id of the video currently in the player
function getVideoPlayerVideoId() {
var videoPlayerVideoSource = getVideoPlayerVideoSource();
if (videoPlayerVideoSource != null) {
return videoPlayerVideoSource.getAttribute("videoid");
}
}
// Gets the duration of the video currently in the player
function getVideoPlayerDuration() {
var videoElement = getVideoPlayer();
if (videoElement != null) {
return videoElement.duration;
}
}
// Gets current watch status of video based on watch button
function getVideoPlayerWatchStatus() {
var videoId = getVideoPlayerVideoId();
var watched = false;
if(document.getElementById(videoId).className != "unseen-icon") {
watched = true;
}
return watched;
}
// Runs on video playback, marks video as watched if video gets to 90% or higher, sends position to api
function onVideoProgress() {
var videoId = getVideoPlayerVideoId();
var currentTime = getVideoPlayerCurrentTime();
var duration = getVideoPlayerDuration();
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);
if (!getVideoPlayerWatchStatus()) { // Check if video is already marked as watched
if (watchedThreshold(currentTime, duration)) {
isWatched(videoId);
}
}
}
}
// Groundwork for saving video position
function sendVideoProgress(videoId, videoProgress) {
var apiEndpoint = "/api/video/";
if (isNaN(videoProgress)) {
videoProgress = 0;
// Runs on video end, marks video as watched
function onVideoEnded() {
var videoId = getVideoPlayerVideoId();
if (!getVideoPlayerWatchStatus()) { // Check if video is already marked as watched
isWatched(videoId);
}
var data = {
"data": [{
"youtube_id": videoId,
"player": {
"progress": videoProgress
}
}]
};
videoData = apiRequest(apiEndpoint, "POST", data);
}
function watchedThreshold(currentTime, duration) {
var watched = false;
if (duration <= 1800){ // If video is less than 30 min
if ((currentTime / duration) >= 0.90) { // Mark as watched at 90%
var watched = true;
}
} else { // If video is more than 30 min
if (currentTime >= (duration - 120)) { // Mark as watched if there is two minutes left
var watched = true;
}
}
return watched;
}
// Runs on video pause. Sends current position.
function onVideoPause() {
var videoId = getVideoPlayerVideoId();
var currentTime = getVideoPlayerCurrentTime();
postVideoProgress(videoId, currentTime);
}
// Format numbers for frontend
@@ -435,27 +509,34 @@ function formatNumbers(number) {
return numberFormatted;
}
// Gets video data in JSON format when passed video ID
// Gets video data when passed video ID
function getVideoData(videoId) {
var apiEndpoint = "/api/video/" + videoId + "/";
videoData = apiRequest(apiEndpoint, "GET");
return videoData.data;
var videoData = apiRequest(apiEndpoint, "GET");
return videoData;
}
// Gets channel data in JSON format when passed channel ID
// Gets channel data when passed channel ID
function getChannelData(channelId) {
var apiEndpoint = "/api/channel/" + channelId + "/";
channelData = apiRequest(apiEndpoint, "GET");
var channelData = apiRequest(apiEndpoint, "GET");
return channelData.data;
}
// Gets playlist data in JSON format when passed playlist ID
// Gets playlist data when passed playlist ID
function getPlaylistData(playlistId) {
var apiEndpoint = "/api/playlist/" + playlistId + "/";
playlistData = apiRequest(apiEndpoint, "GET");
var playlistData = apiRequest(apiEndpoint, "GET");
return playlistData.data;
}
// Get video progress data when passed video ID
function getVideoProgress(videoId) {
var apiEndpoint = "/api/video/" + videoId + "/progress/";
var videoProgress = apiRequest(apiEndpoint, "GET");
return videoProgress;
}
// Given an array of playlist ids it returns an array of subbed playlist ids from that list
function getSubbedPlaylists(videoPlaylists) {
var subbedPlaylists = [];
@@ -467,18 +548,47 @@ function getSubbedPlaylists(videoPlaylists) {
return subbedPlaylists;
}
// Makes api requests when passed an endpoint and method ("GET" or "POST")
// Send video position when given video id and progress in seconds
function postVideoProgress(videoId, videoProgress) {
var apiEndpoint = "/api/video/" + videoId + "/progress/";
var duartion = getVideoPlayerDuration();
if (!isNaN(videoProgress) && duartion != 'undefined') {
var data = {
"position": videoProgress
};
if (videoProgress == 0) {
apiRequest(apiEndpoint, "DELETE");
// console.log("Deleting Video Progress for Video ID: " + videoId + ", Progress: " + videoProgress);
} else if (!getVideoPlayerWatchStatus()) {
apiRequest(apiEndpoint, "POST", data);
// console.log("Saving Video Progress for Video ID: " + videoId + ", Progress: " + videoProgress);
}
}
}
// Makes api requests when passed an endpoint and method ("GET", "POST", "DELETE")
function apiRequest(apiEndpoint, method, data) {
const xhttp = new XMLHttpRequest();
var sessionToken = getCookie("sessionid");
xhttp.open(method, apiEndpoint, false);
xhttp.setRequestHeader("X-CSRFToken", getCookie("csrftoken")); // Used for video progress POST requests
xhttp.setRequestHeader("Authorization", "Token " + sessionToken);
xhttp.setRequestHeader("Content-Type", "application/json");
xhttp.send(JSON.stringify(data));
return JSON.parse(xhttp.responseText);
}
// Gets origin URL
function getURL() {
return window.location.origin;
}
function removePlayer() {
var currentTime = getVideoPlayerCurrentTime();
var duration = getVideoPlayerDuration();
var videoId = getVideoPlayerVideoId();
postVideoProgress(videoId, currentTime);
setProgressBar(videoId, currentTime, duration);
var playerElement = document.getElementById('player');
if (playerElement.hasChildNodes()) {
var youtubeId = playerElement.childNodes[1].getAttribute("data-id");
@@ -494,6 +604,14 @@ function removePlayer() {
}
}
// Sets the progress bar when passed a video id, video progress and video duration
function setProgressBar(videoId, currentTime, duration) {
progressBar = document.getElementById("progress-" + videoId);
progressBarWidth = (currentTime / duration) * 100 + "%";
if (progressBar) {
progressBar.style.width = progressBarWidth;
}
}
// multi search form
function searchMulti(query) {