diff --git a/tubearchivist/api/views.py b/tubearchivist/api/views.py index e026dba..77c345a 100644 --- a/tubearchivist/api/views.py +++ b/tubearchivist/api/views.py @@ -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") diff --git a/tubearchivist/config/management/commands/ta_startup.py b/tubearchivist/config/management/commands/ta_startup.py index b387fb4..16d43d2 100644 --- a/tubearchivist/config/management/commands/ta_startup.py +++ b/tubearchivist/config/management/commands/ta_startup.py @@ -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) diff --git a/tubearchivist/home/src/download/thumbnails.py b/tubearchivist/home/src/download/thumbnails.py index 896f603..a2698fd 100644 --- a/tubearchivist/home/src/download/thumbnails.py +++ b/tubearchivist/home/src/download/thumbnails.py @@ -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): diff --git a/tubearchivist/home/src/frontend/forms.py b/tubearchivist/home/src/frontend/forms.py index 5044456..dde9021 100644 --- a/tubearchivist/home/src/frontend/forms.py +++ b/tubearchivist/home/src/frontend/forms.py @@ -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""" diff --git a/tubearchivist/home/src/index/channel.py b/tubearchivist/home/src/index/channel.py index 3bfaec1..08166f7 100644 --- a/tubearchivist/home/src/index/channel.py +++ b/tubearchivist/home/src/index/channel.py @@ -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): diff --git a/tubearchivist/home/src/index/playlist.py b/tubearchivist/home/src/index/playlist.py index 656da4a..196a884 100644 --- a/tubearchivist/home/src/index/playlist.py +++ b/tubearchivist/home/src/index/playlist.py @@ -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 diff --git a/tubearchivist/home/src/index/reindex.py b/tubearchivist/home/src/index/reindex.py index fe4d986..10a5596 100644 --- a/tubearchivist/home/src/index/reindex.py +++ b/tubearchivist/home/src/index/reindex.py @@ -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"] diff --git a/tubearchivist/home/src/index/video.py b/tubearchivist/home/src/index/video.py index 1b258ae..41f0114 100644 --- a/tubearchivist/home/src/index/video.py +++ b/tubearchivist/home/src/index/video.py @@ -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): diff --git a/tubearchivist/home/src/ta/urlparser.py b/tubearchivist/home/src/ta/urlparser.py index 04429fa..743af98 100644 --- a/tubearchivist/home/src/ta/urlparser.py +++ b/tubearchivist/home/src/ta/urlparser.py @@ -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}") diff --git a/tubearchivist/home/templates/home/playlist.html b/tubearchivist/home/templates/home/playlist.html index ab2d788..ea7baba 100644 --- a/tubearchivist/home/templates/home/playlist.html +++ b/tubearchivist/home/templates/home/playlist.html @@ -11,13 +11,18 @@ {% if request.user|has_group:"admin" or request.user.is_staff %}
- add-icon + add-icon
{% csrf_token %} {{ subscribe_form }}
+
+ {% csrf_token %} + {{ create_form }} + +
{% endif %} @@ -51,14 +56,18 @@
-

{{ playlist.playlist_channel }}

+ {% if playlist.playlist_type != "custom" %} +

{{ playlist.playlist_channel }}

+ {% endif %}

{{ playlist.playlist_name }}

Last refreshed: {{ playlist.playlist_last_refresh }}

- {% if playlist.playlist_subscribed %} - - {% else %} - - {% endif %} + {% if playlist.playlist_type != "custom" %} + {% if playlist.playlist_subscribed %} + + {% else %} + + {% endif %} + {% endif %}
{% endfor %} diff --git a/tubearchivist/home/templates/home/playlist_id.html b/tubearchivist/home/templates/home/playlist_id.html index 5fbe194..9a234c1 100644 --- a/tubearchivist/home/templates/home/playlist_id.html +++ b/tubearchivist/home/templates/home/playlist_id.html @@ -9,37 +9,42 @@

{{ playlist_info.playlist_name }}

-
-
- - channel-thumb - -
-
-

{{ channel_info.channel_name }}

- {% if channel_info.channel_subs >= 1000000 %} - Subscribers: {{ channel_info.channel_subs|intword }} - {% else %} - Subscribers: {{ channel_info.channel_subs|intcomma }} - {% endif %} -
-
+ {% if playlist_info.playlist_type != "custom" %} +
+
+ + channel-thumb + +
+
+

{{ channel_info.channel_name }}

+ {% if channel_info.channel_subs >= 1000000 %} + Subscribers: {{ channel_info.channel_subs|intword }} + {% else %} + Subscribers: {{ channel_info.channel_subs|intcomma }} + {% endif %} +
+
+ {% endif %} +

Last refreshed: {{ playlist_info.playlist_last_refresh }}

-

Playlist: - {% if playlist_info.playlist_subscribed %} - {% if request.user|has_group:"admin" or request.user.is_staff %} - - {% endif %} - {% else %} - - {% endif %} -

- {% if playlist_info.playlist_active %} -

Youtube: Active

- {% else %} -

Youtube: Deactivated

+ {% if playlist_info.playlist_type != "custom" %} +

Playlist: + {% if playlist_info.playlist_subscribed %} + {% if request.user|has_group:"admin" or request.user.is_staff %} + + {% endif %} + {% else %} + + {% endif %} +

+ {% if playlist_info.playlist_active %} +

Youtube: Active

+ {% else %} +

Youtube: Deactivated

+ {% endif %} {% endif %}
@@ -63,7 +68,9 @@

Reindex scheduled

{% else %}
- + {% if playlist_info.playlist_type != "custom" %} + + {% endif %}
{% endif %} @@ -138,15 +145,35 @@ {% endif %} {{ video.published }} | {{ video.player.duration_str }}
-
-

{{ video.title }}

+
+
+ {% if playlist_info.playlist_type == "custom" %} +

{{ video.channel.channel_name }}

+ {% endif %} +

{{ video.title }}

+
+ {% if playlist_info.playlist_type == "custom" %} + {% if pagination %} + {% if pagination.last_page > 0 %} + dot-menu-icon + {% else %} + dot-menu-icon + {% endif %} + {% else %} + dot-menu-icon + {% endif %} + {% endif %}
{% endfor %} {% else %}

No videos found...

-

Try going to the downloads page to start the scan and download tasks.

+ {% if playlist_info.playlist_type == "custom" %} +

Try going to the home page to add videos to this playlist.

+ {% else %} +

Try going to the downloads page to start the scan and download tasks.

+ {% endif %} {% endif %}
diff --git a/tubearchivist/home/templates/home/video.html b/tubearchivist/home/templates/home/video.html index b9ad04a..bb78267 100644 --- a/tubearchivist/home/templates/home/video.html +++ b/tubearchivist/home/templates/home/video.html @@ -95,6 +95,7 @@ Are you sure? {% endif %} +
diff --git a/tubearchivist/home/views.py b/tubearchivist/home/views.py index 36acf17..e87cc27 100644 --- a/tubearchivist/home/views.py +++ b/tubearchivist/home/views.py @@ -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") diff --git a/tubearchivist/static/css/style.css b/tubearchivist/static/css/style.css index 4856105..67eba90 100644 --- a/tubearchivist/static/css/style.css +++ b/tubearchivist/static/css/style.css @@ -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%; } diff --git a/tubearchivist/static/img/default-playlist-thumb.jpg b/tubearchivist/static/img/default-playlist-thumb.jpg new file mode 100644 index 0000000..6bbaa22 Binary files /dev/null and b/tubearchivist/static/img/default-playlist-thumb.jpg differ diff --git a/tubearchivist/static/img/icon-arrow-bottom.svg b/tubearchivist/static/img/icon-arrow-bottom.svg new file mode 100644 index 0000000..e829250 --- /dev/null +++ b/tubearchivist/static/img/icon-arrow-bottom.svg @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/tubearchivist/static/img/icon-arrow-down.svg b/tubearchivist/static/img/icon-arrow-down.svg new file mode 100644 index 0000000..0d7adba --- /dev/null +++ b/tubearchivist/static/img/icon-arrow-down.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/tubearchivist/static/img/icon-arrow-top.svg b/tubearchivist/static/img/icon-arrow-top.svg new file mode 100644 index 0000000..bb4b80c --- /dev/null +++ b/tubearchivist/static/img/icon-arrow-top.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/tubearchivist/static/img/icon-arrow-up.svg b/tubearchivist/static/img/icon-arrow-up.svg new file mode 100644 index 0000000..71c9652 --- /dev/null +++ b/tubearchivist/static/img/icon-arrow-up.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/tubearchivist/static/img/icon-dot-menu.svg b/tubearchivist/static/img/icon-dot-menu.svg new file mode 100644 index 0000000..9aa4411 --- /dev/null +++ b/tubearchivist/static/img/icon-dot-menu.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/tubearchivist/static/img/icon-remove.svg b/tubearchivist/static/img/icon-remove.svg new file mode 100644 index 0000000..f73c5a6 --- /dev/null +++ b/tubearchivist/static/img/icon-remove.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/tubearchivist/static/script.js b/tubearchivist/static/script.js index b1e6ad1..8bf726c 100644 --- a/tubearchivist/static/script.js +++ b/tubearchivist/static/script.js @@ -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 = '

Add video to...

'; + + for(let i = 0; i < playlists.length; i++) { + let obj = playlists[i]; + form_code += '

'+obj.playlist_name+'

'; + } + + form_code += '

Create playlist

'; + 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 = '

Move Video

'; + + form_code += ''; + form_code += ''; + form_code += ''; + form_code += ''; + form_code += ''; + + + form_code += '
'; + 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';