Feature 590 custom playlist (#620)
* add remove custom playlist * custom playlist page, move video controls * align to existing code patterns * cleanup * resolve merge conflict * cleanup * cleanup * polish * polish * some fixes for lint * resolve merge conflict * bugfix on delete video/playlist/channel - preserve custom playlist but delete corresponding videos in custom playlist * cleanup * ./deploy.sh validate isort fix - validate runs clean now * sync to latest master branch * sync to master * updates per admin guidance. sync to master * attempt to resolve merge conflict * attempt to resolve merge conflict - reintroduce changes to file. * validate playlist_type * validate playlist custom action * move custom id creation to view * stricter custom playlist matching * revert unreachable playlist delete check * undo unneeded playlist matching --------- Co-authored-by: Simon <simobilleter@gmail.com>
|
@ -40,7 +40,7 @@ from home.tasks import (
|
||||||
run_restore_backup,
|
run_restore_backup,
|
||||||
subscribe_to,
|
subscribe_to,
|
||||||
)
|
)
|
||||||
from rest_framework import permissions
|
from rest_framework import permissions, status
|
||||||
from rest_framework.authentication import (
|
from rest_framework.authentication import (
|
||||||
SessionAuthentication,
|
SessionAuthentication,
|
||||||
TokenAuthentication,
|
TokenAuthentication,
|
||||||
|
@ -462,12 +462,26 @@ class PlaylistApiListView(ApiBaseView):
|
||||||
|
|
||||||
search_base = "ta_playlist/_search/"
|
search_base = "ta_playlist/_search/"
|
||||||
permission_classes = [AdminWriteOnly]
|
permission_classes = [AdminWriteOnly]
|
||||||
|
valid_playlist_type = ["regular", "custom"]
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""handle get request"""
|
"""handle get request"""
|
||||||
self.data.update(
|
playlist_type = request.GET.get("playlist_type", None)
|
||||||
{"sort": [{"playlist_name.keyword": {"order": "asc"}}]}
|
query = {"sort": [{"playlist_name.keyword": {"order": "asc"}}]}
|
||||||
)
|
if playlist_type is not None:
|
||||||
|
if playlist_type not in self.valid_playlist_type:
|
||||||
|
message = f"invalid playlist_type {playlist_type}"
|
||||||
|
return Response({"message": message}, status=400)
|
||||||
|
|
||||||
|
query.update(
|
||||||
|
{
|
||||||
|
"query": {
|
||||||
|
"term": {"playlist_type": {"value": playlist_type}}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.data.update(query)
|
||||||
self.get_document_list(request)
|
self.get_document_list(request)
|
||||||
return Response(self.response)
|
return Response(self.response)
|
||||||
|
|
||||||
|
@ -511,6 +525,7 @@ class PlaylistApiView(ApiBaseView):
|
||||||
|
|
||||||
search_base = "ta_playlist/_doc/"
|
search_base = "ta_playlist/_doc/"
|
||||||
permission_classes = [AdminWriteOnly]
|
permission_classes = [AdminWriteOnly]
|
||||||
|
valid_custom_actions = ["create", "remove", "up", "down", "top", "bottom"]
|
||||||
|
|
||||||
def get(self, request, playlist_id):
|
def get(self, request, playlist_id):
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
|
@ -518,6 +533,27 @@ class PlaylistApiView(ApiBaseView):
|
||||||
self.get_document(playlist_id)
|
self.get_document(playlist_id)
|
||||||
return Response(self.response, status=self.status_code)
|
return Response(self.response, status=self.status_code)
|
||||||
|
|
||||||
|
def post(self, request, playlist_id):
|
||||||
|
"""post to custom playlist to add a video to list"""
|
||||||
|
playlist = YoutubePlaylist(playlist_id)
|
||||||
|
if not playlist.is_custom_playlist():
|
||||||
|
message = f"playlist with ID {playlist_id} is not custom"
|
||||||
|
return Response({"message": message}, status=400)
|
||||||
|
|
||||||
|
action = request.data.get("action")
|
||||||
|
if action not in self.valid_custom_actions:
|
||||||
|
message = f"invalid action: {action}"
|
||||||
|
return Response({"message": message}, status=400)
|
||||||
|
|
||||||
|
video_id = request.data.get("video_id")
|
||||||
|
if action == "create":
|
||||||
|
playlist.add_video_to_playlist(video_id)
|
||||||
|
else:
|
||||||
|
hide = UserConfig(request.user.id).get_value("hide_watched")
|
||||||
|
playlist.move_video(video_id, action, hide_watched=hide)
|
||||||
|
|
||||||
|
return Response({"success": True}, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
def delete(self, request, playlist_id):
|
def delete(self, request, playlist_id):
|
||||||
"""delete playlist"""
|
"""delete playlist"""
|
||||||
print(f"{playlist_id}: delete playlist")
|
print(f"{playlist_id}: delete playlist")
|
||||||
|
|
|
@ -8,6 +8,7 @@ import os
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
from django.core.management.base import BaseCommand, CommandError
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
from home.src.es.connect import ElasticWrap
|
||||||
from home.src.es.index_setup import ElasitIndexWrap
|
from home.src.es.index_setup import ElasitIndexWrap
|
||||||
from home.src.es.snapshot import ElasticSnapshot
|
from home.src.es.snapshot import ElasticSnapshot
|
||||||
from home.src.ta.config import AppConfig, ReleaseVersion
|
from home.src.ta.config import AppConfig, ReleaseVersion
|
||||||
|
@ -44,6 +45,7 @@ class Command(BaseCommand):
|
||||||
self._mig_index_setup()
|
self._mig_index_setup()
|
||||||
self._mig_snapshot_check()
|
self._mig_snapshot_check()
|
||||||
self._mig_move_users_to_es()
|
self._mig_move_users_to_es()
|
||||||
|
self._mig_custom_playlist()
|
||||||
|
|
||||||
def _sync_redis_state(self):
|
def _sync_redis_state(self):
|
||||||
"""make sure redis gets new config.json values"""
|
"""make sure redis gets new config.json values"""
|
||||||
|
@ -242,3 +244,36 @@ class Command(BaseCommand):
|
||||||
" ✓ Settings for all users migrated to ES"
|
" ✓ Settings for all users migrated to ES"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _mig_custom_playlist(self):
|
||||||
|
"""migration for custom playlist"""
|
||||||
|
self.stdout.write("[MIGRATION] custom playlist")
|
||||||
|
data = {
|
||||||
|
"query": {
|
||||||
|
"bool": {"must_not": [{"exists": {"field": "playlist_type"}}]}
|
||||||
|
},
|
||||||
|
"script": {"source": "ctx._source['playlist_type'] = 'regular'"},
|
||||||
|
}
|
||||||
|
path = "ta_playlist/_update_by_query"
|
||||||
|
response, status_code = ElasticWrap(path).post(data=data)
|
||||||
|
if status_code == 200:
|
||||||
|
updated = response.get("updated", 0)
|
||||||
|
if updated:
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(
|
||||||
|
f" ✓ {updated} playlist_type updated in ta_playlist"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(
|
||||||
|
" no playlist_type needed updating in ta_playlist"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
message = " 🗙 ta_playlist playlist_type update failed"
|
||||||
|
self.stdout.write(self.style.ERROR(message))
|
||||||
|
self.stdout.write(response)
|
||||||
|
sleep(60)
|
||||||
|
raise CommandError(message)
|
||||||
|
|
|
@ -75,7 +75,7 @@ class ThumbManagerBase:
|
||||||
app_root, "static/img/default-video-thumb.jpg"
|
app_root, "static/img/default-video-thumb.jpg"
|
||||||
),
|
),
|
||||||
"playlist": os.path.join(
|
"playlist": os.path.join(
|
||||||
app_root, "static/img/default-video-thumb.jpg"
|
app_root, "static/img/default-playlist-thumb.jpg"
|
||||||
),
|
),
|
||||||
"icon": os.path.join(
|
"icon": os.path.join(
|
||||||
app_root, "static/img/default-channel-icon.jpg"
|
app_root, "static/img/default-channel-icon.jpg"
|
||||||
|
@ -202,7 +202,18 @@ class ThumbManager(ThumbManagerBase):
|
||||||
if skip_existing and os.path.exists(thumb_path):
|
if skip_existing and os.path.exists(thumb_path):
|
||||||
return
|
return
|
||||||
|
|
||||||
img_raw = self.download_raw(url)
|
img_raw = (
|
||||||
|
self.download_raw(url)
|
||||||
|
if not isinstance(url, str) or url.startswith("http")
|
||||||
|
else Image.open(os.path.join(self.CACHE_DIR, url))
|
||||||
|
)
|
||||||
|
width, height = img_raw.size
|
||||||
|
|
||||||
|
if not width / height == 16 / 9:
|
||||||
|
new_height = width / 16 * 9
|
||||||
|
offset = (height - new_height) / 2
|
||||||
|
img_raw = img_raw.crop((0, offset, width, height - offset))
|
||||||
|
img_raw = img_raw.resize((336, 189))
|
||||||
img_raw.convert("RGB").save(thumb_path)
|
img_raw.convert("RGB").save(thumb_path)
|
||||||
|
|
||||||
def delete_video_thumb(self):
|
def delete_video_thumb(self):
|
||||||
|
|
|
@ -265,6 +265,20 @@ class SubscribeToPlaylistForm(forms.Form):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CreatePlaylistForm(forms.Form):
|
||||||
|
"""text area form to create a single custom playlist"""
|
||||||
|
|
||||||
|
create = forms.CharField(
|
||||||
|
label="Or create custom playlist",
|
||||||
|
widget=forms.Textarea(
|
||||||
|
attrs={
|
||||||
|
"rows": 1,
|
||||||
|
"placeholder": "Input playlist name",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ChannelOverwriteForm(forms.Form):
|
class ChannelOverwriteForm(forms.Form):
|
||||||
"""custom overwrites for channel settings"""
|
"""custom overwrites for channel settings"""
|
||||||
|
|
||||||
|
|
|
@ -213,6 +213,7 @@ class YoutubeChannel(YouTubeItem):
|
||||||
all_playlists = self.get_indexed_playlists()
|
all_playlists = self.get_indexed_playlists()
|
||||||
for playlist in all_playlists:
|
for playlist in all_playlists:
|
||||||
playlist_id = playlist["playlist_id"]
|
playlist_id = playlist["playlist_id"]
|
||||||
|
playlist = YoutubePlaylist(playlist_id)
|
||||||
YoutubePlaylist(playlist_id).delete_metadata()
|
YoutubePlaylist(playlist_id).delete_metadata()
|
||||||
|
|
||||||
def delete_channel(self):
|
def delete_channel(self):
|
||||||
|
|
|
@ -66,6 +66,7 @@ class YoutubePlaylist(YouTubeItem):
|
||||||
"playlist_thumbnail": playlist_thumbnail,
|
"playlist_thumbnail": playlist_thumbnail,
|
||||||
"playlist_description": self.youtube_meta["description"] or False,
|
"playlist_description": self.youtube_meta["description"] or False,
|
||||||
"playlist_last_refresh": int(datetime.now().timestamp()),
|
"playlist_last_refresh": int(datetime.now().timestamp()),
|
||||||
|
"playlist_type": "regular",
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_entries(self, playlistend=False):
|
def get_entries(self, playlistend=False):
|
||||||
|
@ -178,6 +179,7 @@ class YoutubePlaylist(YouTubeItem):
|
||||||
|
|
||||||
def delete_metadata(self):
|
def delete_metadata(self):
|
||||||
"""delete metadata for playlist"""
|
"""delete metadata for playlist"""
|
||||||
|
self.delete_videos_metadata()
|
||||||
script = (
|
script = (
|
||||||
"ctx._source.playlist.removeAll("
|
"ctx._source.playlist.removeAll("
|
||||||
+ "Collections.singleton(params.playlist)) "
|
+ "Collections.singleton(params.playlist)) "
|
||||||
|
@ -195,6 +197,30 @@ class YoutubePlaylist(YouTubeItem):
|
||||||
_, _ = ElasticWrap("ta_video/_update_by_query").post(data)
|
_, _ = ElasticWrap("ta_video/_update_by_query").post(data)
|
||||||
self.del_in_es()
|
self.del_in_es()
|
||||||
|
|
||||||
|
def is_custom_playlist(self):
|
||||||
|
self.get_from_es()
|
||||||
|
return self.json_data["playlist_type"] == "custom"
|
||||||
|
|
||||||
|
def delete_videos_metadata(self, channel_id=None):
|
||||||
|
"""delete video metadata for a specific channel"""
|
||||||
|
self.get_from_es()
|
||||||
|
playlist = self.json_data["playlist_entries"]
|
||||||
|
i = 0
|
||||||
|
while i < len(playlist):
|
||||||
|
video_id = playlist[i]["youtube_id"]
|
||||||
|
video = YoutubeVideo(video_id)
|
||||||
|
video.get_from_es()
|
||||||
|
if (
|
||||||
|
channel_id is None
|
||||||
|
or video.json_data["channel"]["channel_id"] == channel_id
|
||||||
|
):
|
||||||
|
playlist.pop(i)
|
||||||
|
self.remove_playlist_from_video(video_id)
|
||||||
|
i -= 1
|
||||||
|
i += 1
|
||||||
|
self.set_playlist_thumbnail()
|
||||||
|
self.upload_to_es()
|
||||||
|
|
||||||
def delete_videos_playlist(self):
|
def delete_videos_playlist(self):
|
||||||
"""delete playlist with all videos"""
|
"""delete playlist with all videos"""
|
||||||
print(f"{self.youtube_id}: delete playlist")
|
print(f"{self.youtube_id}: delete playlist")
|
||||||
|
@ -208,3 +234,159 @@ class YoutubePlaylist(YouTubeItem):
|
||||||
YoutubeVideo(youtube_id).delete_media_file()
|
YoutubeVideo(youtube_id).delete_media_file()
|
||||||
|
|
||||||
self.delete_metadata()
|
self.delete_metadata()
|
||||||
|
|
||||||
|
def create(self, name):
|
||||||
|
self.json_data = {
|
||||||
|
"playlist_id": self.youtube_id,
|
||||||
|
"playlist_active": False,
|
||||||
|
"playlist_name": name,
|
||||||
|
"playlist_last_refresh": int(datetime.now().timestamp()),
|
||||||
|
"playlist_entries": [],
|
||||||
|
"playlist_type": "custom",
|
||||||
|
"playlist_channel": None,
|
||||||
|
"playlist_channel_id": None,
|
||||||
|
"playlist_description": False,
|
||||||
|
"playlist_thumbnail": False,
|
||||||
|
"playlist_subscribed": False,
|
||||||
|
}
|
||||||
|
self.upload_to_es()
|
||||||
|
self.get_playlist_art()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def add_video_to_playlist(self, video_id):
|
||||||
|
self.get_from_es()
|
||||||
|
video_metadata = self.get_video_metadata(video_id)
|
||||||
|
video_metadata["idx"] = len(self.json_data["playlist_entries"])
|
||||||
|
|
||||||
|
if not self.playlist_entries_contains(video_id):
|
||||||
|
self.json_data["playlist_entries"].append(video_metadata)
|
||||||
|
self.json_data["playlist_last_refresh"] = int(
|
||||||
|
datetime.now().timestamp()
|
||||||
|
)
|
||||||
|
self.set_playlist_thumbnail()
|
||||||
|
self.upload_to_es()
|
||||||
|
video = YoutubeVideo(video_id)
|
||||||
|
video.get_from_es()
|
||||||
|
if "playlist" not in video.json_data:
|
||||||
|
video.json_data["playlist"] = []
|
||||||
|
video.json_data["playlist"].append(self.youtube_id)
|
||||||
|
video.upload_to_es()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def remove_playlist_from_video(self, video_id):
|
||||||
|
video = YoutubeVideo(video_id)
|
||||||
|
video.get_from_es()
|
||||||
|
if video.json_data is not None and "playlist" in video.json_data:
|
||||||
|
video.json_data["playlist"].remove(self.youtube_id)
|
||||||
|
video.upload_to_es()
|
||||||
|
|
||||||
|
def move_video(self, video_id, action, hide_watched=False):
|
||||||
|
self.get_from_es()
|
||||||
|
video_index = self.get_video_index(video_id)
|
||||||
|
playlist = self.json_data["playlist_entries"]
|
||||||
|
item = playlist[video_index]
|
||||||
|
playlist.pop(video_index)
|
||||||
|
if action == "remove":
|
||||||
|
self.remove_playlist_from_video(item["youtube_id"])
|
||||||
|
else:
|
||||||
|
if action == "up":
|
||||||
|
while True:
|
||||||
|
video_index = max(0, video_index - 1)
|
||||||
|
if (
|
||||||
|
not hide_watched
|
||||||
|
or video_index == 0
|
||||||
|
or (
|
||||||
|
not self.get_video_is_watched(
|
||||||
|
playlist[video_index]["youtube_id"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
):
|
||||||
|
break
|
||||||
|
elif action == "down":
|
||||||
|
while True:
|
||||||
|
video_index = min(len(playlist), video_index + 1)
|
||||||
|
if (
|
||||||
|
not hide_watched
|
||||||
|
or video_index == len(playlist)
|
||||||
|
or (
|
||||||
|
not self.get_video_is_watched(
|
||||||
|
playlist[video_index - 1]["youtube_id"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
):
|
||||||
|
break
|
||||||
|
elif action == "top":
|
||||||
|
video_index = 0
|
||||||
|
else:
|
||||||
|
video_index = len(playlist)
|
||||||
|
playlist.insert(video_index, item)
|
||||||
|
self.json_data["playlist_last_refresh"] = int(
|
||||||
|
datetime.now().timestamp()
|
||||||
|
)
|
||||||
|
|
||||||
|
for i, item in enumerate(playlist):
|
||||||
|
item["idx"] = i
|
||||||
|
|
||||||
|
self.set_playlist_thumbnail()
|
||||||
|
self.upload_to_es()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def del_video(self, video_id):
|
||||||
|
playlist = self.json_data["playlist_entries"]
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
while i < len(playlist):
|
||||||
|
if video_id == playlist[i]["youtube_id"]:
|
||||||
|
playlist.pop(i)
|
||||||
|
self.set_playlist_thumbnail()
|
||||||
|
i -= 1
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
def get_video_index(self, video_id):
|
||||||
|
for i, child in enumerate(self.json_data["playlist_entries"]):
|
||||||
|
if child["youtube_id"] == video_id:
|
||||||
|
return i
|
||||||
|
return -1
|
||||||
|
|
||||||
|
def playlist_entries_contains(self, video_id):
|
||||||
|
return (
|
||||||
|
len(
|
||||||
|
list(
|
||||||
|
filter(
|
||||||
|
lambda x: x["youtube_id"] == video_id,
|
||||||
|
self.json_data["playlist_entries"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
> 0
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_video_is_watched(self, video_id):
|
||||||
|
video = YoutubeVideo(video_id)
|
||||||
|
video.get_from_es()
|
||||||
|
return video.json_data["player"]["watched"]
|
||||||
|
|
||||||
|
def set_playlist_thumbnail(self):
|
||||||
|
playlist = self.json_data["playlist_entries"]
|
||||||
|
self.json_data["playlist_thumbnail"] = False
|
||||||
|
|
||||||
|
for video in playlist:
|
||||||
|
url = ThumbManager(video["youtube_id"]).vid_thumb_path()
|
||||||
|
if url is not None:
|
||||||
|
self.json_data["playlist_thumbnail"] = url
|
||||||
|
break
|
||||||
|
self.get_playlist_art()
|
||||||
|
|
||||||
|
def get_video_metadata(self, video_id):
|
||||||
|
video = YoutubeVideo(video_id)
|
||||||
|
video.get_from_es()
|
||||||
|
video_json_data = {
|
||||||
|
"youtube_id": video.json_data["youtube_id"],
|
||||||
|
"title": video.json_data["title"],
|
||||||
|
"uploader": video.json_data["channel"]["channel_name"],
|
||||||
|
"idx": 0,
|
||||||
|
"downloaded": "date_downloaded" in video.json_data
|
||||||
|
and video.json_data["date_downloaded"] > 0,
|
||||||
|
}
|
||||||
|
return video_json_data
|
||||||
|
|
|
@ -363,7 +363,10 @@ class Reindex(ReindexBase):
|
||||||
self._get_all_videos()
|
self._get_all_videos()
|
||||||
playlist = YoutubePlaylist(playlist_id)
|
playlist = YoutubePlaylist(playlist_id)
|
||||||
playlist.get_from_es()
|
playlist.get_from_es()
|
||||||
if not playlist.json_data:
|
if (
|
||||||
|
not playlist.json_data
|
||||||
|
or playlist.json_data["playlist_type"] == "custom"
|
||||||
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
subscribed = playlist.json_data["playlist_subscribed"]
|
subscribed = playlist.json_data["playlist_subscribed"]
|
||||||
|
|
|
@ -319,6 +319,8 @@ class YoutubeVideo(YouTubeItem, YoutubeSubtitle):
|
||||||
playlist.json_data["playlist_entries"][idx].update(
|
playlist.json_data["playlist_entries"][idx].update(
|
||||||
{"downloaded": False}
|
{"downloaded": False}
|
||||||
)
|
)
|
||||||
|
if playlist.json_data["playlist_type"] == "custom":
|
||||||
|
playlist.del_video(self.youtube_id)
|
||||||
playlist.upload_to_es()
|
playlist.upload_to_es()
|
||||||
|
|
||||||
def delete_subtitles(self, subtitles=False):
|
def delete_subtitles(self, subtitles=False):
|
||||||
|
|
|
@ -92,7 +92,7 @@ class Parser:
|
||||||
item_type = "video"
|
item_type = "video"
|
||||||
elif len_id_str == 24:
|
elif len_id_str == 24:
|
||||||
item_type = "channel"
|
item_type = "channel"
|
||||||
elif len_id_str in (34, 26, 18):
|
elif len_id_str in (34, 26, 18) or id_str.startswith("TA_playlist_"):
|
||||||
item_type = "playlist"
|
item_type = "playlist"
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"not a valid id_str: {id_str}")
|
raise ValueError(f"not a valid id_str: {id_str}")
|
||||||
|
|
|
@ -11,13 +11,18 @@
|
||||||
{% if request.user|has_group:"admin" or request.user.is_staff %}
|
{% if request.user|has_group:"admin" or request.user.is_staff %}
|
||||||
|
|
||||||
<div class="title-split-form">
|
<div class="title-split-form">
|
||||||
<img id="animate-icon" onclick="showForm()" src="{% static 'img/icon-add.svg' %}" alt="add-icon" title="Subscribe to Playlists">
|
<img id="animate-icon" onclick="showForm();showForm('hidden-form2')" src="{% static 'img/icon-add.svg' %}" alt="add-icon" title="Subscribe to Playlists">
|
||||||
<div class="show-form">
|
<div class="show-form">
|
||||||
<form id="hidden-form" action="/playlist/" method="post">
|
<form id="hidden-form" action="/playlist/" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ subscribe_form }}
|
{{ subscribe_form }}
|
||||||
<button type="submit">Subscribe</button>
|
<button type="submit">Subscribe</button>
|
||||||
</form>
|
</form>
|
||||||
|
<form id="hidden-form2" action="/playlist/" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ create_form }}
|
||||||
|
<button type="submit">Create</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -51,14 +56,18 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="playlist-desc {{ view_style }}">
|
<div class="playlist-desc {{ view_style }}">
|
||||||
<a href="{% url 'channel_id' playlist.playlist_channel_id %}"><h3>{{ playlist.playlist_channel }}</h3></a>
|
{% if playlist.playlist_type != "custom" %}
|
||||||
|
<a href="{% url 'channel_id' playlist.playlist_channel_id %}"><h3>{{ playlist.playlist_channel }}</h3></a>
|
||||||
|
{% endif %}
|
||||||
<a href="{% url 'playlist_id' playlist.playlist_id %}"><h2>{{ playlist.playlist_name }}</h2></a>
|
<a href="{% url 'playlist_id' playlist.playlist_id %}"><h2>{{ playlist.playlist_name }}</h2></a>
|
||||||
<p>Last refreshed: {{ playlist.playlist_last_refresh }}</p>
|
<p>Last refreshed: {{ playlist.playlist_last_refresh }}</p>
|
||||||
{% if playlist.playlist_subscribed %}
|
{% if playlist.playlist_type != "custom" %}
|
||||||
<button class="unsubscribe" type="button" data-type="playlist" data-subscribe="" data-id="{{ playlist.playlist_id }}" onclick="subscribeStatus(this)" title="Unsubscribe from {{ playlist.playlist_name }}">Unsubscribe</button>
|
{% if playlist.playlist_subscribed %}
|
||||||
{% else %}
|
<button class="unsubscribe" type="button" data-type="playlist" data-subscribe="" data-id="{{ playlist.playlist_id }}" onclick="subscribeStatus(this)" title="Unsubscribe from {{ playlist.playlist_name }}">Unsubscribe</button>
|
||||||
<button type="button" data-type="playlist" data-subscribe="true" data-id="{{ playlist.playlist_id }}" onclick="subscribeStatus(this)" title="Subscribe to {{ playlist.playlist_name }}">Subscribe</button>
|
{% else %}
|
||||||
{% endif %}
|
<button type="button" data-type="playlist" data-subscribe="true" data-id="{{ playlist.playlist_id }}" onclick="subscribeStatus(this)" title="Subscribe to {{ playlist.playlist_name }}">Subscribe</button>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -9,37 +9,42 @@
|
||||||
<h1>{{ playlist_info.playlist_name }}</h1>
|
<h1>{{ playlist_info.playlist_name }}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-box info-box-3">
|
<div class="info-box info-box-3">
|
||||||
<div class="info-box-item">
|
{% if playlist_info.playlist_type != "custom" %}
|
||||||
<div class="round-img">
|
<div class="info-box-item">
|
||||||
<a href="{% url 'channel_id' channel_info.channel_id %}">
|
<div class="round-img">
|
||||||
<img src="/cache/channels/{{ channel_info.channel_id }}_thumb.jpg" alt="channel-thumb">
|
<a href="{% url 'channel_id' channel_info.channel_id %}">
|
||||||
</a>
|
<img src="/cache/channels/{{ channel_info.channel_id }}_thumb.jpg" alt="channel-thumb">
|
||||||
</div>
|
</a>
|
||||||
<div>
|
</div>
|
||||||
<h3><a href="{% url 'channel_id' channel_info.channel_id %}">{{ channel_info.channel_name }}</a></h3>
|
<div>
|
||||||
{% if channel_info.channel_subs >= 1000000 %}
|
<h3><a href="{% url 'channel_id' channel_info.channel_id %}">{{ channel_info.channel_name }}</a></h3>
|
||||||
<span>Subscribers: {{ channel_info.channel_subs|intword }}</span>
|
{% if channel_info.channel_subs >= 1000000 %}
|
||||||
{% else %}
|
<span>Subscribers: {{ channel_info.channel_subs|intword }}</span>
|
||||||
<span>Subscribers: {{ channel_info.channel_subs|intcomma }}</span>
|
{% else %}
|
||||||
{% endif %}
|
<span>Subscribers: {{ channel_info.channel_subs|intcomma }}</span>
|
||||||
</div>
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="info-box-item">
|
<div class="info-box-item">
|
||||||
<div>
|
<div>
|
||||||
<p>Last refreshed: {{ playlist_info.playlist_last_refresh }}</p>
|
<p>Last refreshed: {{ playlist_info.playlist_last_refresh }}</p>
|
||||||
<p>Playlist:
|
{% if playlist_info.playlist_type != "custom" %}
|
||||||
{% if playlist_info.playlist_subscribed %}
|
<p>Playlist:
|
||||||
{% if request.user|has_group:"admin" or request.user.is_staff %}
|
{% if playlist_info.playlist_subscribed %}
|
||||||
<button class="unsubscribe" type="button" data-type="playlist" data-subscribe="" data-id="{{ playlist_info.playlist_id }}" onclick="subscribeStatus(this)" title="Unsubscribe from {{ playlist_info.playlist_name }}">Unsubscribe</button>
|
{% if request.user|has_group:"admin" or request.user.is_staff %}
|
||||||
{% endif %}
|
<button class="unsubscribe" type="button" data-type="playlist" data-subscribe="" data-id="{{ playlist_info.playlist_id }}" onclick="subscribeStatus(this)" title="Unsubscribe from {{ playlist_info.playlist_name }}">Unsubscribe</button>
|
||||||
{% else %}
|
{% endif %}
|
||||||
<button type="button" data-type="playlist" data-subscribe="true" data-id="{{ playlist_info.playlist_id }}" onclick="subscribeStatus(this)" title="Subscribe to {{ playlist_info.playlist_name }}">Subscribe</button>
|
{% else %}
|
||||||
{% endif %}
|
<button type="button" data-type="playlist" data-subscribe="true" data-id="{{ playlist_info.playlist_id }}" onclick="subscribeStatus(this)" title="Subscribe to {{ playlist_info.playlist_name }}">Subscribe</button>
|
||||||
</p>
|
{% endif %}
|
||||||
{% if playlist_info.playlist_active %}
|
</p>
|
||||||
<p>Youtube: <a href="https://www.youtube.com/playlist?list={{ playlist_info.playlist_id }}" target="_blank">Active</a></p>
|
{% if playlist_info.playlist_active %}
|
||||||
{% else %}
|
<p>Youtube: <a href="https://www.youtube.com/playlist?list={{ playlist_info.playlist_id }}" target="_blank">Active</a></p>
|
||||||
<p>Youtube: Deactivated</p>
|
{% else %}
|
||||||
|
<p>Youtube: Deactivated</p>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<button onclick="deleteConfirm()" id="delete-item">Delete Playlist</button>
|
<button onclick="deleteConfirm()" id="delete-item">Delete Playlist</button>
|
||||||
<div class="delete-confirm" id="delete-button">
|
<div class="delete-confirm" id="delete-button">
|
||||||
|
@ -63,7 +68,9 @@
|
||||||
<p>Reindex scheduled</p>
|
<p>Reindex scheduled</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div id="reindex-button" class="button-box">
|
<div id="reindex-button" class="button-box">
|
||||||
<button data-id="{{ playlist_info.playlist_id }}" data-type="playlist" onclick="reindex(this)" title="Reindex Playlist {{ playlist_info.playlist_name }}">Reindex</button>
|
{% if playlist_info.playlist_type != "custom" %}
|
||||||
|
<button data-id="{{ playlist_info.playlist_id }}" data-type="playlist" onclick="reindex(this)" title="Reindex Playlist {{ playlist_info.playlist_name }}">Reindex</button>
|
||||||
|
{% endif %}
|
||||||
<button data-id="{{ playlist_info.playlist_id }}" data-type="playlist" data-extract-videos="true" onclick="reindex(this)" title="Reindex Videos of {{ playlist_info.playlist_name }}">Reindex Videos</button>
|
<button data-id="{{ playlist_info.playlist_id }}" data-type="playlist" data-extract-videos="true" onclick="reindex(this)" title="Reindex Videos of {{ playlist_info.playlist_name }}">Reindex Videos</button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -138,15 +145,35 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<span>{{ video.published }} | {{ video.player.duration_str }}</span>
|
<span>{{ video.published }} | {{ video.player.duration_str }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="video-desc-details">
|
||||||
<a class="video-more" href="{% url 'video' video.youtube_id %}"><h2>{{ video.title }}</h2></a>
|
<div>
|
||||||
|
{% if playlist_info.playlist_type == "custom" %}
|
||||||
|
<a href="{% url 'channel_id' video.channel.channel_id %}"><h3>{{ video.channel.channel_name }}</h3></a>
|
||||||
|
{% endif %}
|
||||||
|
<a class="video-more" href="{% url 'video' video.youtube_id %}"><h2>{{ video.title }}</h2></a>
|
||||||
|
</div>
|
||||||
|
{% if playlist_info.playlist_type == "custom" %}
|
||||||
|
{% if pagination %}
|
||||||
|
{% if pagination.last_page > 0 %}
|
||||||
|
<img id="{{ video.youtube_id }}-button" src="{% static 'img/icon-dot-menu.svg' %}" alt="dot-menu-icon" data-id="{{ video.youtube_id }}" data-context="video" onclick="showCustomPlaylistMenu(this,'{{playlist_info.playlist_id}}',{{pagination.current_page}},{{pagination.last_page}})" class="dot-button" title="More actions">
|
||||||
|
{% else %}
|
||||||
|
<img id="{{ video.youtube_id }}-button" src="{% static 'img/icon-dot-menu.svg' %}" alt="dot-menu-icon" data-id="{{ video.youtube_id }}" data-context="video" onclick="showCustomPlaylistMenu(this,'{{playlist_info.playlist_id}}',{{pagination.current_page}},{{pagination.current_page}})" class="dot-button" title="More actions">
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<img id="{{ video.youtube_id }}-button" src="{% static 'img/icon-dot-menu.svg' %}" alt="dot-menu-icon" data-id="{{ video.youtube_id }}" data-context="video" onclick="showCustomPlaylistMenu(this,'{{playlist_info.playlist_id}}',0,0)" class="dot-button" title="More actions">
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<h2>No videos found...</h2>
|
<h2>No videos found...</h2>
|
||||||
<p>Try going to the <a href="{% url 'downloads' %}">downloads page</a> to start the scan and download tasks.</p>
|
{% if playlist_info.playlist_type == "custom" %}
|
||||||
|
<p>Try going to the <a href="{% url 'home' %}">home page</a> to add videos to this playlist.</p>
|
||||||
|
{% else %}
|
||||||
|
<p>Try going to the <a href="{% url 'downloads' %}">downloads page</a> to start the scan and download tasks.</p>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -95,6 +95,7 @@
|
||||||
<span>Are you sure? </span><button class="danger-button" onclick="deleteVideo(this)" data-id="{{ video.youtube_id }}" data-redirect = "{{ video.channel.channel_id }}">Delete</button> <button onclick="cancelDelete()">Cancel</button>
|
<span>Are you sure? </span><button class="danger-button" onclick="deleteVideo(this)" data-id="{{ video.youtube_id }}" data-redirect = "{{ video.channel.channel_id }}">Delete</button> <button onclick="cancelDelete()">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<button id="{{ video.youtube_id }}-button" data-id="{{ video.youtube_id }}" data-context="video" onclick="showAddToPlaylistMenu(this)">Add To Playlist</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-box-item">
|
<div class="info-box-item">
|
||||||
|
|
|
@ -6,6 +6,7 @@ Functionality:
|
||||||
|
|
||||||
import enum
|
import enum
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
import uuid
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
from api.src.search_processor import SearchProcess, process_aggs
|
from api.src.search_processor import SearchProcess, process_aggs
|
||||||
|
@ -27,6 +28,7 @@ from home.src.frontend.forms import (
|
||||||
AddToQueueForm,
|
AddToQueueForm,
|
||||||
ApplicationSettingsForm,
|
ApplicationSettingsForm,
|
||||||
ChannelOverwriteForm,
|
ChannelOverwriteForm,
|
||||||
|
CreatePlaylistForm,
|
||||||
CustomAuthForm,
|
CustomAuthForm,
|
||||||
MultiSearchForm,
|
MultiSearchForm,
|
||||||
SchedulerSettingsForm,
|
SchedulerSettingsForm,
|
||||||
|
@ -740,12 +742,12 @@ class PlaylistIdView(ArchivistResultsView):
|
||||||
# playlist details
|
# playlist details
|
||||||
es_path = f"ta_playlist/_doc/{playlist_id}"
|
es_path = f"ta_playlist/_doc/{playlist_id}"
|
||||||
playlist_info = self.single_lookup(es_path)
|
playlist_info = self.single_lookup(es_path)
|
||||||
|
channel_info = None
|
||||||
# channel details
|
if playlist_info["playlist_type"] != "custom":
|
||||||
channel_id = playlist_info["playlist_channel_id"]
|
# channel details
|
||||||
es_path = f"ta_channel/_doc/{channel_id}"
|
channel_id = playlist_info["playlist_channel_id"]
|
||||||
channel_info = self.single_lookup(es_path)
|
es_path = f"ta_channel/_doc/{channel_id}"
|
||||||
|
channel_info = self.single_lookup(es_path)
|
||||||
return playlist_info, channel_info
|
return playlist_info, channel_info
|
||||||
|
|
||||||
def _update_view_data(self, playlist_id, playlist_info):
|
def _update_view_data(self, playlist_id, playlist_info):
|
||||||
|
@ -803,6 +805,7 @@ class PlaylistView(ArchivistResultsView):
|
||||||
{
|
{
|
||||||
"title": "Playlists",
|
"title": "Playlists",
|
||||||
"subscribe_form": SubscribeToPlaylistForm(),
|
"subscribe_form": SubscribeToPlaylistForm(),
|
||||||
|
"create_form": CreatePlaylistForm(),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -837,12 +840,19 @@ class PlaylistView(ArchivistResultsView):
|
||||||
@method_decorator(user_passes_test(check_admin), name="dispatch")
|
@method_decorator(user_passes_test(check_admin), name="dispatch")
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def post(request):
|
def post(request):
|
||||||
"""handle post from search form"""
|
"""handle post from subscribe or create form"""
|
||||||
subscribe_form = SubscribeToPlaylistForm(data=request.POST)
|
if request.POST.get("create") is not None:
|
||||||
if subscribe_form.is_valid():
|
create_form = CreatePlaylistForm(data=request.POST)
|
||||||
url_str = request.POST.get("subscribe")
|
if create_form.is_valid():
|
||||||
print(url_str)
|
name = request.POST.get("create")
|
||||||
subscribe_to.delay(url_str, expected_type="playlist")
|
playlist_id = f"TA_playlist_{uuid.uuid4()}"
|
||||||
|
YoutubePlaylist(playlist_id).create(name)
|
||||||
|
else:
|
||||||
|
subscribe_form = SubscribeToPlaylistForm(data=request.POST)
|
||||||
|
if subscribe_form.is_valid():
|
||||||
|
url_str = request.POST.get("subscribe")
|
||||||
|
print(url_str)
|
||||||
|
subscribe_to.delay(url_str, expected_type="playlist")
|
||||||
|
|
||||||
sleep(1)
|
sleep(1)
|
||||||
return redirect("playlist")
|
return redirect("playlist")
|
||||||
|
|
|
@ -371,11 +371,23 @@ button:hover {
|
||||||
filter: var(--img-filter);
|
filter: var(--img-filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.video-popup-menu {
|
||||||
|
border-top: 2px solid;
|
||||||
|
border-color: var(--accent-font-dark);
|
||||||
|
margin: 5px 0;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
#hidden-form {
|
#hidden-form {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#hidden-form button {
|
#hidden-form2 {
|
||||||
|
display: none;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#hidden-form button, #hidden-form2 button {
|
||||||
margin-right: 1rem;
|
margin-right: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -564,6 +576,12 @@ video:-webkit-full-screen {
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.video-popup-menu img.move-video-button {
|
||||||
|
width: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
filter: var(--img-filter);
|
||||||
|
}
|
||||||
|
|
||||||
.video-desc a {
|
.video-desc a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
@ -592,7 +610,13 @@ video:-webkit-full-screen {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.video-desc-details {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
.watch-button,
|
.watch-button,
|
||||||
|
.dot-button,
|
||||||
.close-button {
|
.close-button {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
filter: var(--img-filter);
|
filter: var(--img-filter);
|
||||||
|
@ -682,6 +706,19 @@ video:-webkit-full-screen {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.video-popup-menu img {
|
||||||
|
width: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
filter: var(--img-filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.video-popup-menu-close-button {
|
||||||
|
cursor: pointer;
|
||||||
|
filter: var(--img-filter);
|
||||||
|
float:right;
|
||||||
|
}
|
||||||
|
|
||||||
.description-text {
|
.description-text {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
After Width: | Height: | Size: 24 KiB |
|
@ -0,0 +1,14 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 27.5.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1"
|
||||||
|
id="svg1303" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:svg="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 500 500"
|
||||||
|
style="enable-background:new 0 0 500 500;" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<path d="M231.3,382c4.8,5.3,11.6,8.3,18.7,8.3c7.1,0,13.9-3,18.7-8.3l112.1-124.2c6.7-7.4,8.4-18,4.3-27.1
|
||||||
|
c-4-9.1-13.1-14.9-23-14.9h-52.9V64.9c0-13.9-11.3-25.2-25.2-25.2h-68c-13.9,0-25.2,11.3-25.2,25.2v150.9h-52.9
|
||||||
|
c-10,0-19,5.9-23,14.9c-4,9.1-2.3,19.7,4.3,27.1L231.3,382z"/>
|
||||||
|
<path d="M436.1,408.8H63.9c-13.6,0-24.7,11.1-24.7,24.7c0,13.6,11.1,24.7,24.7,24.7h372.2c13.6,0,24.7-11.1,24.7-24.7
|
||||||
|
C460.8,419.9,449.7,408.8,436.1,408.8z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg viewBox="0 0 500 500" style="enable-background:new 0 0 500 500;" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g style="" transform="matrix(0.9999999999999999, 0, 0, -0.9999999999999999, 0, 497.9000392526395)">
|
||||||
|
<path d="M 231.3 48 C 236.1 42.7 242.9 39.7 250 39.7 C 257.1 39.7 263.9 42.7 268.7 48 L 380.8 172.2 C 387.5 179.6 389.2 190.2 385.1 199.3 C 381.1 208.4 372 214.2 362.1 214.2 L 309.2 214.2 L 309.2 365.1 C 309.2 379 297.9 390.3 284 390.3 L 216 390.3 C 202.1 390.3 190.8 379 190.8 365.1 L 190.8 214.2 L 137.9 214.2 C 127.9 214.2 118.9 208.3 114.9 199.3 C 110.9 190.2 112.6 179.6 119.2 172.2 L 231.3 48 Z" style=""/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 678 B |
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg viewBox="0 0 500 500" style="enable-background:new 0 0 500 500;" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g style="" transform="matrix(0.9999999999999999, 0, 0, -0.9999999999999999, 0, 497.9000392526395)">
|
||||||
|
<path d="M231.3,382c4.8,5.3,11.6,8.3,18.7,8.3c7.1,0,13.9-3,18.7-8.3l112.1-124.2c6.7-7.4,8.4-18,4.3-27.1 c-4-9.1-13.1-14.9-23-14.9h-52.9V64.9c0-13.9-11.3-25.2-25.2-25.2h-68c-13.9,0-25.2,11.3-25.2,25.2v150.9h-52.9 c-10,0-19,5.9-23,14.9c-4,9.1-2.3,19.7,4.3,27.1L231.3,382z"/>
|
||||||
|
<path d="M436.1,408.8H63.9c-13.6,0-24.7,11.1-24.7,24.7c0,13.6,11.1,24.7,24.7,24.7h372.2c13.6,0,24.7-11.1,24.7-24.7 C460.8,419.9,449.7,408.8,436.1,408.8z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 698 B |
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg viewBox="0 0 500 500" style="enable-background:new 0 0 500 500;" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g style="" transform="matrix(0.9999999999999999, 0, 0, -0.9999999999999999, 0, 497.9000392526395)">
|
||||||
|
<path d="M231.3,382c4.8,5.3,11.6,8.3,18.7,8.3c7.1,0,13.9-3,18.7-8.3l112.1-124.2c6.7-7.4,8.4-18,4.3-27.1 c-4-9.1-13.1-14.9-23-14.9h-52.9V64.9c0-13.9-11.3-25.2-25.2-25.2h-68c-13.9,0-25.2,11.3-25.2,25.2v150.9h-52.9 c-10,0-19,5.9-23,14.9c-4,9.1-2.3,19.7,4.3,27.1L231.3,382z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 538 B |
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#000000" class="bi bi-three-dots-vertical">
|
||||||
|
<path d="M9.5 13a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 405 B |
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg viewBox="0 0 500 500" style="enable-background:new 0 0 500 500;" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M 408.514 358.563 L 303.835 255.003 L 408.441 149.115 C 424.1 133.406 424.1 107.662 408.441 91.953 C 392.783 76.245 367.12 76.245 351.463 91.953 L 246.857 197.841 L 142.323 91.881 C 126.665 76.172 101.002 76.172 85.344 91.881 C 69.613 107.662 69.613 133.334 85.272 149.115 L 189.805 255.003 L 84.401 359.291 C 68.743 375 68.743 400.744 84.401 416.453 C 100.06 432.161 125.722 432.161 141.381 416.453 L 246.784 312.165 L 351.39 415.726 C 367.048 431.434 392.711 431.434 408.369 415.726 C 424.172 400.017 424.172 374.345 408.514 358.563 Z" style="" transform="matrix(0.9999999999999999, 0, 0, 0.9999999999999999, 0, 0)"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 782 B |
|
@ -197,6 +197,137 @@ function addToQueue(autostart = false) {
|
||||||
showForm();
|
showForm();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//shows the video sub menu popup
|
||||||
|
function showAddToPlaylistMenu(input1) {
|
||||||
|
let dataId, playlists, form_code, buttonId;
|
||||||
|
dataId = input1.getAttribute('data-id');
|
||||||
|
buttonId = input1.getAttribute('id');
|
||||||
|
playlists = getCustomPlaylists();
|
||||||
|
|
||||||
|
//hide the invoking button
|
||||||
|
input1.style.visibility = "hidden";
|
||||||
|
|
||||||
|
//show the form
|
||||||
|
form_code = '<div class="video-popup-menu"><img src="/static/img/icon-close.svg" class="video-popup-menu-close-button" title="Close menu" onclick="removeDotMenu(this, \''+buttonId+'\')"/><h3>Add video to...</h3>';
|
||||||
|
|
||||||
|
for(let i = 0; i < playlists.length; i++) {
|
||||||
|
let obj = playlists[i];
|
||||||
|
form_code += '<p onclick="addToCustomPlaylist(this, \''+dataId+'\',\''+obj.playlist_id+'\')"><img class="p-button" src="/static/img/icon-unseen.svg"/>'+obj.playlist_name+'</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
form_code += '<p><a href="/playlist">Create playlist</a></p></div>';
|
||||||
|
input1.parentNode.parentNode.innerHTML += form_code;
|
||||||
|
}
|
||||||
|
|
||||||
|
//handles user action of adding a video to a custom playlist
|
||||||
|
function addToCustomPlaylist(input, video_id, playlist_id) {
|
||||||
|
let apiEndpoint = '/api/playlist/' + playlist_id + '/';
|
||||||
|
let data = { "action": "create", "video_id": video_id };
|
||||||
|
apiRequest(apiEndpoint, 'POST', data);
|
||||||
|
|
||||||
|
//mark the item added in the ui
|
||||||
|
input.firstChild.src='/static/img/icon-seen.svg';
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeDotMenu(input1, button_id) {
|
||||||
|
|
||||||
|
//show the menu button
|
||||||
|
document.getElementById(button_id).style.visibility = "visible";
|
||||||
|
|
||||||
|
//remove the form
|
||||||
|
input1.parentNode.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
//shows the video sub menu popup on custom playlist page
|
||||||
|
function showCustomPlaylistMenu(input1, playlist_id, current_page, last_page) {
|
||||||
|
let dataId, form_code, buttonId;
|
||||||
|
dataId = input1.getAttribute('data-id');
|
||||||
|
buttonId = input1.getAttribute('id');
|
||||||
|
|
||||||
|
//hide the invoking button
|
||||||
|
input1.style.visibility = "hidden";
|
||||||
|
|
||||||
|
//show the form
|
||||||
|
form_code = '<div class="video-popup-menu"><img src="/static/img/icon-close.svg" class="video-popup-menu-close-button" title="Close menu" onclick="removeDotMenu(this, \''+buttonId+'\')"/><h3>Move Video</h3>';
|
||||||
|
|
||||||
|
form_code += '<img class="move-video-button" data-id="'+dataId+'" data-context="top" onclick="moveCustomPlaylistVideo(this,\''+playlist_id+'\','+current_page+','+last_page+')" src="/static/img/icon-arrow-top.svg" title="Move to top"/>';
|
||||||
|
form_code += '<img class="move-video-button" data-id="'+dataId+'" data-context="up" onclick="moveCustomPlaylistVideo(this,\''+playlist_id+'\','+current_page+','+last_page+')" src="/static/img/icon-arrow-up.svg" title="Move up"/>';
|
||||||
|
form_code += '<img class="move-video-button" data-id="'+dataId+'" data-context="down" onclick="moveCustomPlaylistVideo(this,\''+playlist_id+'\','+current_page+','+last_page+')" src="/static/img/icon-arrow-down.svg" title="Move down"/>';
|
||||||
|
form_code += '<img class="move-video-button" data-id="'+dataId+'" data-context="bottom" onclick="moveCustomPlaylistVideo(this,\''+playlist_id+'\','+current_page+','+last_page+')" src="/static/img/icon-arrow-bottom.svg" title="Move to bottom"/>';
|
||||||
|
form_code += '<img class="move-video-button" data-id="'+dataId+'" data-context="remove" onclick="moveCustomPlaylistVideo(this,\''+playlist_id+'\','+current_page+','+last_page+')" src="/static/img/icon-remove.svg" title="Remove from playlist"/>';
|
||||||
|
|
||||||
|
|
||||||
|
form_code += '</div>';
|
||||||
|
input1.parentNode.parentNode.innerHTML += form_code;
|
||||||
|
}
|
||||||
|
|
||||||
|
//process custom playlist form actions
|
||||||
|
function moveCustomPlaylistVideo(input1, playlist_id, current_page, last_page) {
|
||||||
|
let dataId, dataContext;
|
||||||
|
dataId = input1.getAttribute('data-id');
|
||||||
|
dataContext = input1.getAttribute('data-context');
|
||||||
|
|
||||||
|
let apiEndpoint = '/api/playlist/' + playlist_id + '/';
|
||||||
|
let data = { "action": dataContext, "video_id": dataId };
|
||||||
|
apiRequest(apiEndpoint, 'POST', data);
|
||||||
|
|
||||||
|
let itemDom = input1.parentElement.parentElement.parentElement;
|
||||||
|
let listDom = itemDom.parentElement;
|
||||||
|
|
||||||
|
if (dataContext === "up")
|
||||||
|
{
|
||||||
|
let sibling = itemDom.previousElementSibling;
|
||||||
|
if (sibling !== null)
|
||||||
|
{
|
||||||
|
sibling.before(itemDom);
|
||||||
|
}
|
||||||
|
else if (current_page > 1)
|
||||||
|
{
|
||||||
|
itemDom.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (dataContext === "down")
|
||||||
|
{
|
||||||
|
let sibling = itemDom.nextElementSibling;
|
||||||
|
if (sibling !== null)
|
||||||
|
{
|
||||||
|
sibling.after(itemDom);
|
||||||
|
}
|
||||||
|
else if (current_page !== last_page)
|
||||||
|
{
|
||||||
|
itemDom.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (dataContext === "top")
|
||||||
|
{
|
||||||
|
let sibling = listDom.firstElementChild;
|
||||||
|
if (sibling !== null)
|
||||||
|
{
|
||||||
|
sibling.before(itemDom);
|
||||||
|
}
|
||||||
|
if (current_page > 1)
|
||||||
|
{
|
||||||
|
itemDom.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (dataContext === "bottom")
|
||||||
|
{
|
||||||
|
let sibling = listDom.lastElementChild;
|
||||||
|
if (sibling !== null)
|
||||||
|
{
|
||||||
|
sibling.after(itemDom);
|
||||||
|
}
|
||||||
|
if (current_page !== last_page)
|
||||||
|
{
|
||||||
|
itemDom.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (dataContext === "remove")
|
||||||
|
{
|
||||||
|
itemDom.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function toIgnore(button) {
|
function toIgnore(button) {
|
||||||
let youtube_id = button.getAttribute('data-id');
|
let youtube_id = button.getAttribute('data-id');
|
||||||
let apiEndpoint = '/api/download/' + youtube_id + '/';
|
let apiEndpoint = '/api/download/' + youtube_id + '/';
|
||||||
|
@ -773,6 +904,13 @@ function getPlaylistData(playlistId) {
|
||||||
return playlistData.data;
|
return playlistData.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gets custom playlists
|
||||||
|
function getCustomPlaylists() {
|
||||||
|
let apiEndpoint = '/api/playlist/?playlist_type=custom';
|
||||||
|
let playlistData = apiRequest(apiEndpoint, 'GET');
|
||||||
|
return playlistData.data;
|
||||||
|
}
|
||||||
|
|
||||||
// Get video progress data when passed video ID
|
// Get video progress data when passed video ID
|
||||||
function getVideoProgress(videoId) {
|
function getVideoProgress(videoId) {
|
||||||
let apiEndpoint = '/api/video/' + videoId + '/progress/';
|
let apiEndpoint = '/api/video/' + videoId + '/progress/';
|
||||||
|
@ -1383,8 +1521,10 @@ function textExpandButtonVisibilityUpdate() {
|
||||||
document.addEventListener('readystatechange', textExpandButtonVisibilityUpdate);
|
document.addEventListener('readystatechange', textExpandButtonVisibilityUpdate);
|
||||||
window.addEventListener('resize', textExpandButtonVisibilityUpdate);
|
window.addEventListener('resize', textExpandButtonVisibilityUpdate);
|
||||||
|
|
||||||
function showForm() {
|
function showForm(id) {
|
||||||
let formElement = document.getElementById('hidden-form');
|
|
||||||
|
let id2 = id === undefined ? 'hidden-form' : id;
|
||||||
|
let formElement = document.getElementById(id2);
|
||||||
let displayStyle = formElement.style.display;
|
let displayStyle = formElement.style.display;
|
||||||
if (displayStyle === '') {
|
if (displayStyle === '') {
|
||||||
formElement.style.display = 'block';
|
formElement.style.display = 'block';
|
||||||
|
|