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,
|
||||
subscribe_to,
|
||||
)
|
||||
from rest_framework import permissions
|
||||
from rest_framework import permissions, status
|
||||
from rest_framework.authentication import (
|
||||
SessionAuthentication,
|
||||
TokenAuthentication,
|
||||
@ -462,12 +462,26 @@ class PlaylistApiListView(ApiBaseView):
|
||||
|
||||
search_base = "ta_playlist/_search/"
|
||||
permission_classes = [AdminWriteOnly]
|
||||
valid_playlist_type = ["regular", "custom"]
|
||||
|
||||
def get(self, request):
|
||||
"""handle get request"""
|
||||
self.data.update(
|
||||
{"sort": [{"playlist_name.keyword": {"order": "asc"}}]}
|
||||
)
|
||||
playlist_type = request.GET.get("playlist_type", None)
|
||||
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)
|
||||
return Response(self.response)
|
||||
|
||||
@ -511,6 +525,7 @@ class PlaylistApiView(ApiBaseView):
|
||||
|
||||
search_base = "ta_playlist/_doc/"
|
||||
permission_classes = [AdminWriteOnly]
|
||||
valid_custom_actions = ["create", "remove", "up", "down", "top", "bottom"]
|
||||
|
||||
def get(self, request, playlist_id):
|
||||
# pylint: disable=unused-argument
|
||||
@ -518,6 +533,27 @@ class PlaylistApiView(ApiBaseView):
|
||||
self.get_document(playlist_id)
|
||||
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):
|
||||
"""delete playlist"""
|
||||
print(f"{playlist_id}: delete playlist")
|
||||
|
@ -8,6 +8,7 @@ import os
|
||||
from time import sleep
|
||||
|
||||
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.snapshot import ElasticSnapshot
|
||||
from home.src.ta.config import AppConfig, ReleaseVersion
|
||||
@ -44,6 +45,7 @@ class Command(BaseCommand):
|
||||
self._mig_index_setup()
|
||||
self._mig_snapshot_check()
|
||||
self._mig_move_users_to_es()
|
||||
self._mig_custom_playlist()
|
||||
|
||||
def _sync_redis_state(self):
|
||||
"""make sure redis gets new config.json values"""
|
||||
@ -242,3 +244,36 @@ class Command(BaseCommand):
|
||||
" ✓ 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"
|
||||
),
|
||||
"playlist": os.path.join(
|
||||
app_root, "static/img/default-video-thumb.jpg"
|
||||
app_root, "static/img/default-playlist-thumb.jpg"
|
||||
),
|
||||
"icon": os.path.join(
|
||||
app_root, "static/img/default-channel-icon.jpg"
|
||||
@ -202,7 +202,18 @@ class ThumbManager(ThumbManagerBase):
|
||||
if skip_existing and os.path.exists(thumb_path):
|
||||
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)
|
||||
|
||||
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):
|
||||
"""custom overwrites for channel settings"""
|
||||
|
||||
|
@ -213,6 +213,7 @@ class YoutubeChannel(YouTubeItem):
|
||||
all_playlists = self.get_indexed_playlists()
|
||||
for playlist in all_playlists:
|
||||
playlist_id = playlist["playlist_id"]
|
||||
playlist = YoutubePlaylist(playlist_id)
|
||||
YoutubePlaylist(playlist_id).delete_metadata()
|
||||
|
||||
def delete_channel(self):
|
||||
|
@ -66,6 +66,7 @@ class YoutubePlaylist(YouTubeItem):
|
||||
"playlist_thumbnail": playlist_thumbnail,
|
||||
"playlist_description": self.youtube_meta["description"] or False,
|
||||
"playlist_last_refresh": int(datetime.now().timestamp()),
|
||||
"playlist_type": "regular",
|
||||
}
|
||||
|
||||
def get_entries(self, playlistend=False):
|
||||
@ -178,6 +179,7 @@ class YoutubePlaylist(YouTubeItem):
|
||||
|
||||
def delete_metadata(self):
|
||||
"""delete metadata for playlist"""
|
||||
self.delete_videos_metadata()
|
||||
script = (
|
||||
"ctx._source.playlist.removeAll("
|
||||
+ "Collections.singleton(params.playlist)) "
|
||||
@ -195,6 +197,30 @@ class YoutubePlaylist(YouTubeItem):
|
||||
_, _ = ElasticWrap("ta_video/_update_by_query").post(data)
|
||||
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):
|
||||
"""delete playlist with all videos"""
|
||||
print(f"{self.youtube_id}: delete playlist")
|
||||
@ -208,3 +234,159 @@ class YoutubePlaylist(YouTubeItem):
|
||||
YoutubeVideo(youtube_id).delete_media_file()
|
||||
|
||||
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()
|
||||
playlist = YoutubePlaylist(playlist_id)
|
||||
playlist.get_from_es()
|
||||
if not playlist.json_data:
|
||||
if (
|
||||
not playlist.json_data
|
||||
or playlist.json_data["playlist_type"] == "custom"
|
||||
):
|
||||
return
|
||||
|
||||
subscribed = playlist.json_data["playlist_subscribed"]
|
||||
|
@ -319,6 +319,8 @@ class YoutubeVideo(YouTubeItem, YoutubeSubtitle):
|
||||
playlist.json_data["playlist_entries"][idx].update(
|
||||
{"downloaded": False}
|
||||
)
|
||||
if playlist.json_data["playlist_type"] == "custom":
|
||||
playlist.del_video(self.youtube_id)
|
||||
playlist.upload_to_es()
|
||||
|
||||
def delete_subtitles(self, subtitles=False):
|
||||
|
@ -92,7 +92,7 @@ class Parser:
|
||||
item_type = "video"
|
||||
elif len_id_str == 24:
|
||||
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"
|
||||
else:
|
||||
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 %}
|
||||
|
||||
<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">
|
||||
<form id="hidden-form" action="/playlist/" method="post">
|
||||
{% csrf_token %}
|
||||
{{ subscribe_form }}
|
||||
<button type="submit">Subscribe</button>
|
||||
</form>
|
||||
<form id="hidden-form2" action="/playlist/" method="post">
|
||||
{% csrf_token %}
|
||||
{{ create_form }}
|
||||
<button type="submit">Create</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@ -51,14 +56,18 @@
|
||||
</a>
|
||||
</div>
|
||||
<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>
|
||||
<p>Last refreshed: {{ playlist.playlist_last_refresh }}</p>
|
||||
{% if playlist.playlist_subscribed %}
|
||||
<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>
|
||||
{% else %}
|
||||
<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 %}
|
||||
{% if playlist.playlist_type != "custom" %}
|
||||
{% if playlist.playlist_subscribed %}
|
||||
<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>
|
||||
{% else %}
|
||||
<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>
|
||||
{% endfor %}
|
||||
|
@ -9,37 +9,42 @@
|
||||
<h1>{{ playlist_info.playlist_name }}</h1>
|
||||
</div>
|
||||
<div class="info-box info-box-3">
|
||||
<div class="info-box-item">
|
||||
<div class="round-img">
|
||||
<a href="{% url 'channel_id' channel_info.channel_id %}">
|
||||
<img src="/cache/channels/{{ channel_info.channel_id }}_thumb.jpg" alt="channel-thumb">
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<h3><a href="{% url 'channel_id' channel_info.channel_id %}">{{ channel_info.channel_name }}</a></h3>
|
||||
{% if channel_info.channel_subs >= 1000000 %}
|
||||
<span>Subscribers: {{ channel_info.channel_subs|intword }}</span>
|
||||
{% else %}
|
||||
<span>Subscribers: {{ channel_info.channel_subs|intcomma }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if playlist_info.playlist_type != "custom" %}
|
||||
<div class="info-box-item">
|
||||
<div class="round-img">
|
||||
<a href="{% url 'channel_id' channel_info.channel_id %}">
|
||||
<img src="/cache/channels/{{ channel_info.channel_id }}_thumb.jpg" alt="channel-thumb">
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<h3><a href="{% url 'channel_id' channel_info.channel_id %}">{{ channel_info.channel_name }}</a></h3>
|
||||
{% if channel_info.channel_subs >= 1000000 %}
|
||||
<span>Subscribers: {{ channel_info.channel_subs|intword }}</span>
|
||||
{% else %}
|
||||
<span>Subscribers: {{ channel_info.channel_subs|intcomma }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="info-box-item">
|
||||
<div>
|
||||
<p>Last refreshed: {{ playlist_info.playlist_last_refresh }}</p>
|
||||
<p>Playlist:
|
||||
{% if playlist_info.playlist_subscribed %}
|
||||
{% if request.user|has_group:"admin" or request.user.is_staff %}
|
||||
<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>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<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>
|
||||
{% endif %}
|
||||
</p>
|
||||
{% if playlist_info.playlist_active %}
|
||||
<p>Youtube: <a href="https://www.youtube.com/playlist?list={{ playlist_info.playlist_id }}" target="_blank">Active</a></p>
|
||||
{% else %}
|
||||
<p>Youtube: Deactivated</p>
|
||||
{% if playlist_info.playlist_type != "custom" %}
|
||||
<p>Playlist:
|
||||
{% if playlist_info.playlist_subscribed %}
|
||||
{% if request.user|has_group:"admin" or request.user.is_staff %}
|
||||
<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>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<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>
|
||||
{% endif %}
|
||||
</p>
|
||||
{% if playlist_info.playlist_active %}
|
||||
<p>Youtube: <a href="https://www.youtube.com/playlist?list={{ playlist_info.playlist_id }}" target="_blank">Active</a></p>
|
||||
{% else %}
|
||||
<p>Youtube: Deactivated</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<button onclick="deleteConfirm()" id="delete-item">Delete Playlist</button>
|
||||
<div class="delete-confirm" id="delete-button">
|
||||
@ -63,7 +68,9 @@
|
||||
<p>Reindex scheduled</p>
|
||||
{% else %}
|
||||
<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>
|
||||
</div>
|
||||
{% endif %}
|
||||
@ -138,15 +145,35 @@
|
||||
{% endif %}
|
||||
<span>{{ video.published }} | {{ video.player.duration_str }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<a class="video-more" href="{% url 'video' video.youtube_id %}"><h2>{{ video.title }}</h2></a>
|
||||
<div class="video-desc-details">
|
||||
<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>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<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 %}
|
||||
</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>
|
||||
</div>
|
||||
{% 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 class="info-box-item">
|
||||
|
@ -6,6 +6,7 @@ Functionality:
|
||||
|
||||
import enum
|
||||
import urllib.parse
|
||||
import uuid
|
||||
from time import sleep
|
||||
|
||||
from api.src.search_processor import SearchProcess, process_aggs
|
||||
@ -27,6 +28,7 @@ from home.src.frontend.forms import (
|
||||
AddToQueueForm,
|
||||
ApplicationSettingsForm,
|
||||
ChannelOverwriteForm,
|
||||
CreatePlaylistForm,
|
||||
CustomAuthForm,
|
||||
MultiSearchForm,
|
||||
SchedulerSettingsForm,
|
||||
@ -740,12 +742,12 @@ class PlaylistIdView(ArchivistResultsView):
|
||||
# playlist details
|
||||
es_path = f"ta_playlist/_doc/{playlist_id}"
|
||||
playlist_info = self.single_lookup(es_path)
|
||||
|
||||
# channel details
|
||||
channel_id = playlist_info["playlist_channel_id"]
|
||||
es_path = f"ta_channel/_doc/{channel_id}"
|
||||
channel_info = self.single_lookup(es_path)
|
||||
|
||||
channel_info = None
|
||||
if playlist_info["playlist_type"] != "custom":
|
||||
# channel details
|
||||
channel_id = playlist_info["playlist_channel_id"]
|
||||
es_path = f"ta_channel/_doc/{channel_id}"
|
||||
channel_info = self.single_lookup(es_path)
|
||||
return playlist_info, channel_info
|
||||
|
||||
def _update_view_data(self, playlist_id, playlist_info):
|
||||
@ -803,6 +805,7 @@ class PlaylistView(ArchivistResultsView):
|
||||
{
|
||||
"title": "Playlists",
|
||||
"subscribe_form": SubscribeToPlaylistForm(),
|
||||
"create_form": CreatePlaylistForm(),
|
||||
}
|
||||
)
|
||||
|
||||
@ -837,12 +840,19 @@ class PlaylistView(ArchivistResultsView):
|
||||
@method_decorator(user_passes_test(check_admin), name="dispatch")
|
||||
@staticmethod
|
||||
def post(request):
|
||||
"""handle post from search form"""
|
||||
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")
|
||||
"""handle post from subscribe or create form"""
|
||||
if request.POST.get("create") is not None:
|
||||
create_form = CreatePlaylistForm(data=request.POST)
|
||||
if create_form.is_valid():
|
||||
name = request.POST.get("create")
|
||||
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)
|
||||
return redirect("playlist")
|
||||
|
@ -371,11 +371,23 @@ button:hover {
|
||||
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 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#hidden-form button {
|
||||
#hidden-form2 {
|
||||
display: none;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#hidden-form button, #hidden-form2 button {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
@ -564,6 +576,12 @@ video:-webkit-full-screen {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.video-popup-menu img.move-video-button {
|
||||
width: 24px;
|
||||
cursor: pointer;
|
||||
filter: var(--img-filter);
|
||||
}
|
||||
|
||||
.video-desc a {
|
||||
text-decoration: none;
|
||||
text-align: left;
|
||||
@ -592,7 +610,13 @@ video:-webkit-full-screen {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.video-desc-details {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.watch-button,
|
||||
.dot-button,
|
||||
.close-button {
|
||||
cursor: pointer;
|
||||
filter: var(--img-filter);
|
||||
@ -682,6 +706,19 @@ video:-webkit-full-screen {
|
||||
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 {
|
||||
width: 100%;
|
||||
}
|
||||
|
BIN
tubearchivist/static/img/default-playlist-thumb.jpg
Normal file
After Width: | Height: | Size: 24 KiB |
14
tubearchivist/static/img/icon-arrow-bottom.svg
Normal file
@ -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 |
6
tubearchivist/static/img/icon-arrow-down.svg
Normal file
@ -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 |
7
tubearchivist/static/img/icon-arrow-top.svg
Normal file
@ -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 |
6
tubearchivist/static/img/icon-arrow-up.svg
Normal file
@ -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 |
4
tubearchivist/static/img/icon-dot-menu.svg
Normal file
@ -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 |
4
tubearchivist/static/img/icon-remove.svg
Normal file
@ -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();
|
||||
}
|
||||
|
||||
//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) {
|
||||
let youtube_id = button.getAttribute('data-id');
|
||||
let apiEndpoint = '/api/download/' + youtube_id + '/';
|
||||
@ -773,6 +904,13 @@ function getPlaylistData(playlistId) {
|
||||
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
|
||||
function getVideoProgress(videoId) {
|
||||
let apiEndpoint = '/api/video/' + videoId + '/progress/';
|
||||
@ -1383,8 +1521,10 @@ function textExpandButtonVisibilityUpdate() {
|
||||
document.addEventListener('readystatechange', textExpandButtonVisibilityUpdate);
|
||||
window.addEventListener('resize', textExpandButtonVisibilityUpdate);
|
||||
|
||||
function showForm() {
|
||||
let formElement = document.getElementById('hidden-form');
|
||||
function showForm(id) {
|
||||
|
||||
let id2 = id === undefined ? 'hidden-form' : id;
|
||||
let formElement = document.getElementById(id2);
|
||||
let displayStyle = formElement.style.display;
|
||||
if (displayStyle === '') {
|
||||
formElement.style.display = 'block';
|
||||
|