mirror of
https://git.vectorsigma.ru/public/tubearchivist.git
synced 2026-08-08 19:59:20 +00:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6822ed380d | ||
|
|
b28905bbae | ||
|
|
36a2996cda | ||
|
|
b98ccd9dab | ||
|
|
060f0d575e | ||
|
|
99c97a703f | ||
|
|
806448624d | ||
|
|
df777104af | ||
|
|
d88d6d6a61 | ||
|
|
6078d8d276 | ||
|
|
c4d6bb35a3 | ||
|
|
a25b101c3a | ||
|
|
e0db73543e | ||
|
|
4812b8da55 | ||
|
|
8579fb4cc1 | ||
|
|
b8b95f9d79 | ||
|
|
70506ad8f6 | ||
|
|
ec00568008 | ||
|
|
2044dba700 | ||
|
|
241d8326f7 | ||
|
|
4d83af7c14 | ||
|
|
265795f4a9 | ||
|
|
5c3f0d1e5f |
@@ -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" && \
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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/
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user