Compare commits

...

50 Commits

Author SHA1 Message Date
simon
21a76595ec fix new screenshot links in readme 2022-01-09 00:02:02 +07:00
simon
e8a533bbbe add social links and update to v0.1.0 2022-01-08 23:58:41 +07:00
simon
9ad8177b7a retry failed commit 2022-01-08 23:57:51 +07:00
simon
709c7b88f5 wiki refresh for new search functionality 2022-01-08 21:21:49 +07:00
simon
8ba9ced833 move ryd to avoid celery missing redis var 2022-01-08 21:10:38 +07:00
simon
f62e75f343 add social links 2022-01-08 20:14:02 +07:00
simon
1a95797997 improve scheduler validation with regex, #126 2022-01-08 20:04:51 +07:00
simon
c660edd997 bump library versions 2022-01-07 18:35:59 +07:00
simon
9498cf94f3 fix playlist view_origin 2022-01-07 18:30:40 +07:00
simon
099f9dfe00 make search results follow default view styles 2022-01-07 18:29:25 +07:00
simon
1cbe0bbfc0 delete playlists of channel when deleting channel, #118 2022-01-05 15:39:41 +07:00
simon
49cd272557 size limit for ryd missing rating query 2022-01-05 15:10:33 +07:00
simon
1c27934d77 remove redundant lock 2022-01-05 14:51:02 +07:00
simon
c85be45846 extend ryd to refresh when missing average_rating 2022-01-05 14:43:24 +07:00
simon
7a1f77e548 bump library versions 2022-01-05 14:42:28 +07:00
simon
3999d679e3 fix linting 2022-01-05 14:09:35 +07:00
simon
dc2fb65312 add optin returnyoutubedislike.com integration 2022-01-05 14:06:07 +07:00
simon
6aef3dfb96 remove leftovers 2022-01-05 11:10:11 +07:00
simon
49eb565e2a fix sort_by selected attribute and animation toggle 2022-01-02 16:25:05 +07:00
simon
2ce90077b6 fix notification for nownload now button 2022-01-02 15:47:32 +07:00
simon
fd334bcafe fix legacy import alias for yt-dlp 2022-01-02 15:38:45 +07:00
simon
6d16d75ef9 bump es version 2022-01-02 12:24:25 +07:00
simon
d5b3b90202 fix missing auth of search, reduce nginx alias to relevant paths only 2022-01-01 22:17:44 +07:00
simon
37140551d2 improved build cache add nginx after pip 2022-01-01 22:16:34 +07:00
simon
08f74caef0 simplify the sort_order and sort_by and standardize player location in page 2022-01-01 18:13:12 +07:00
simon
f7fce001c4 simplify the subscribe buttons 2022-01-01 17:25:57 +07:00
simon
4915aa0c11 remove now redundant search forms 2021-12-31 13:42:07 +07:00
simon
5616d3ee0d add search page link to top move about to footer 2021-12-31 12:41:54 +07:00
simon
e823d6e1b3 add multi search js functions to dynamically populate results 2021-12-30 22:13:47 +07:00
simon
e8eb7077ed add initial search endpoint and improve results$ 2021-12-30 20:42:42 +07:00
simon
5fb15b79a4 extend SearchForm class with multisearch method for frontend 2021-12-30 15:58:15 +07:00
simon
2a774c3d3e add english analyzer for main text fields for serching 2021-12-30 15:54:20 +07:00
simon
b6424e0e56 update requirements libs 2021-12-30 15:53:38 +07:00
simon
386220ee94 consolidate request attributes into parent class 2021-12-27 17:23:39 +07:00
simon
4c46f0bdbd refactor playlist and playlist-id views inheritance, #116 2021-12-27 16:58:08 +07:00
simon
30cf54c49c refactor channel and channel-id views inheritance, #116 2021-12-27 15:23:52 +07:00
simon
fb512b26ea further consolidate by adding query results in parent class 2021-12-27 12:01:49 +07:00
simon
2ae25540d1 refacter DownloadView to use new inheritance structure, #116 2021-12-27 10:58:49 +07:00
simon
6229d92333 add dedicated default_view for playlists 2021-12-26 22:51:37 +07:00
simon
6e348892f2 refactor home view by inherinting from dedicated view classes, #115 #116 2021-12-26 22:49:00 +07:00
simon
665d0a1b24 bump versions 2021-12-26 18:22:04 +07:00
simon
42f4f4cb6f improved sort order mobile layout on home 2021-12-18 18:38:01 +07:00
simon
2d6cc43a9e better label placement on toggle box 2021-12-18 18:22:57 +07:00
simon
bf0b07f3d7 fix error when restarting player with same video 2021-12-18 17:15:53 +07:00
simon
88ebb6d648 fix autodelete error on empty download queue 2021-12-18 17:05:49 +07:00
simon
3015e59a29 fix styling of sub unsub frontent feedback with span tag 2021-12-18 16:56:32 +07:00
simon
7a3609e611 add unstable tag to sync_docker deploy 2021-12-18 16:35:01 +07:00
simon
ad8d75c3e8 add autodelete videos to ignore list, #112 2021-12-17 23:28:53 +07:00
simon
f36ae6c4b9 hotfix: explicitly set DJANGO_DEBUG env, #111 2021-12-17 23:02:38 +07:00
simon
8678cdcadb add missing favicon to login template 2021-12-17 23:00:30 +07:00
46 changed files with 1070 additions and 1047 deletions

View File

@@ -1,6 +1,6 @@
# build the tube archivist image from default python slim image
FROM python:3.9.9-slim-bullseye
FROM python:3.10.1-slim-bullseye
ARG TARGETPLATFORM
ENV PYTHONUNBUFFERED 1
@@ -25,9 +25,6 @@ RUN if [ "$TARGETPLATFORM" = "linux/amd64" ] ; then \
apt-get -y update && apt-get -y install --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/* \
; fi
# copy config files
COPY nginx.conf /etc/nginx/conf.d/
# make folders
RUN mkdir /cache
RUN mkdir /youtube
@@ -37,6 +34,9 @@ RUN mkdir /app
COPY ./tubearchivist/requirements.txt /requirements.txt
RUN pip install --no-cache-dir -r requirements.txt --src /usr/local/src
# copy config files
COPY nginx.conf /etc/nginx/conf.d/
# copy application into container
COPY ./tubearchivist /app
COPY ./run.sh /app

View File

@@ -7,6 +7,7 @@
* [Core functionality](#core-functionality)
* [Screenshots](#screenshots)
* [Problem Tube Archivist tries to solve](#problem-tube-archivist-tries-to-solve)
* [Connect](#connect)
* [Installing and updating](#installing-and-updating)
* [Getting Started](#getting-started)
* [Potential pitfalls](#potential-pitfalls)
@@ -42,6 +43,10 @@
## Problem Tube Archivist tries to solve
Once your YouTube video collection grows, it becomes hard to search and find a specific video. That's where Tube Archivist comes in: By indexing your video collection with metadata from YouTube, you can organize, search and enjoy your archived YouTube videos without hassle offline through a convenient web interface.
## Connect
- [Discord](https://discord.gg/AFwz8nE7BK): Connect with us on our brand new Discord server.
- [r/TubeArchivist](https://www.reddit.com/r/TubeArchivist/): Join our brand new Subreddit.
## Installing and updating
Take a look at the example `docker-compose.yml` file provided. Use the *latest* or the named semantic version tag. The *unstable* tag is for intermediate testing and as the name implies, is **unstable** and not be used on your main installation but in a [testing environment](CONTRIBUTING.md).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 238 KiB

View File

@@ -147,6 +147,7 @@ function sync_docker {
sudo docker buildx build \
--platform linux/amd64,linux/arm64 \
-t bbilly1/tubearchivist:latest \
-t bbilly1/tubearchivist:unstable \
-t bbilly1/tubearchivist:"$VERSION" --push .
# create release tag

View File

@@ -33,7 +33,7 @@ services:
depends_on:
- archivist-es
archivist-es:
image: docker.elastic.co/elasticsearch/elasticsearch:7.16.1
image: docker.elastic.co/elasticsearch/elasticsearch:7.16.2
container_name: archivist-es
restart: always
environment:

View File

@@ -1,6 +1,6 @@
# Channels Overview and Channel Detail Page
The channels are organized on two different levels, similar as the playlists:
The channels are organized on two different levels, similar as the [playlists](Playlists):
## Channels Overview
Accessible at `/channel/` of your Tube Archivist, the **Overview Page** shows a list of all channels you have indexed.
@@ -14,13 +14,13 @@ The **Subscribe to Channels** button <img src="assets/icon-add.png?raw=true" alt
- Enter the video URL for any video and let Tube Archivist extract the channel ID for you. For example *https://www.youtube.com/watch?v=2tdiKTSdE9Y*
- Add one per line.
The search icon <img src="assets/icon-search.png?raw=true" alt="search icon" width="20px" style="margin:0 5px;"> opens a text box to search for indexed channel names. Possible matches will show as you type.
You can search your indexed channels by clicking on the search icon <img src="assets/icon-search.png?raw=true" alt="search icon" width="20px" style="margin:0 5px;">. This will open a dedicated page.
## Channel Detail
Each channel will get a dedicated channel detail page accessible at `/channel/<channel-id>/` of your Tube Archivist. This page shows all the videos you have downloaded from this channel plus additional metadata.
- If you are subscribed to the channel, an *Unsubscribe* button will show, else the *Subscribe* button will show.
- You can *Show* the channel description, that matches with the *About* tab on YouTube.
- The **Mark as Watched** button will mark all videos of this channel as watched.
- The button **Delete Channel** will delete the channel plus all videos of this channel, both media files and metadata.
- The button **Delete Channel** will delete the channel plus all videos of this channel, both media files and metadata additionally this will also delete playlists metadata belonging to that channel.
- The button **Find Playlists** will go out to youtube and look for playlists belonging to this channel and index all with at least one video downloaded. Only do this for channels where you care about playlists as this will slow down indexing new videos for having to check which playlist this belongs to.
- The button **Show Playlists** will go to the [playlists](Playlists) page and filter the list to only show playlists from this channel.

View File

@@ -5,7 +5,7 @@ Accessible at `/downloads/` of your Tube Archivist, this page handles all the do
## Rescan Subscriptions
The **Rescan Subscriptions** icon <img src="assets/icon-rescan.png?raw=true" alt="rescan icon" width="20px" style="margin:0 5px;"> will start a background task to look for new videos from the channels and playlists you are subscribed to. You can define the channel and playlist page size on the [settings page](Settings#subscriptions). With the default page size, expect this process to take around 2-3 seconds for each channel or playlist you are subscribed to. A status message will show the progress.
Then for every video found, **Tube Archivist** will skip the video if it has already been downloaded or if you added it to the *ignored* list before. All the other videos will get added to the download queue. Expect this to take around 1 second for each video as **Tube Archivist** needs to grab some additional metadata. New videos will get added at the bottom of the download queue.
Then for every video found, **Tube Archivist** will skip the video if it has already been downloaded or if you added it to the *ignored* list before. All the other videos will get added to the download queue. Expect this to take around 2 seconds for each video as **Tube Archivist** needs to grab some additional metadata. New videos will get added at the bottom of the download queue.
## Download Queue
The **Start Download** icon <img src="assets/icon-download.png?raw=true" alt="download icon" width="20px" style="margin:0 5px;"> will start the download process starting from the top of the queue. Take a look at the relevant settings on the [Settings Page](Settings#downloads). Once the process started, a progress message will show with additional details and controls:

View File

@@ -2,7 +2,6 @@
Welcome to the official Tube Archivist Wiki. This is an up-to-date documentation of user functionality.
Table of contents:
* [Main](Main): Tube Archivist landing page
* [Channels](Channels): Browse your channels, handle channel subscriptions
* [Playlists](Playlists): Browse your indexed playlists, handle playlist subscriptions
* [Downloads](Downloads): Scanning subscriptions, handle download queue
@@ -21,9 +20,9 @@ Table of contents:
* Clicking on the channel name or the channel icon brings you to the dedicated channel page to show videos from that channel.
* Clicking on a video title brings you to the dedicated video page and shows additional details.
* Clicking on a video thumbnail opens the video player and starts streaming the selected video.
* Hover over the playing video to show additional control options.
* Clicking on the search icon <img src="assets/icon-search.png?raw=true" alt="gridview icon" width="20px" style="margin:0 5px;"> will open a dedicated search page to search over your complete index.
An empty checkbox icon <img src="assets/icon-unseen.png?raw=true" alt="unseen icon" width="20px" style="margin:0 5px;"> will show for videos you haven't marked as watched. Click on it and the icon will change to a filled checkbox <img src="assets/icon-seen.png?raw=true" alt="seen icon" width="20px" style="margin:0 5px;"> indicating it as watched - click again to revert.
When available the <img src="assets/icon-gridview.png?raw=true" alt="gridview icon" width="20px" style="margin:0 5px;"> gridview icon will display the list in a grid, the <img src="assets/icon-listview.png?raw=true" alt="listview icon" width="20px" style="margin:0 5px;"> listview icon will arrange the items in a list.
When available the <img src="assets/icon-gridview.png?raw=true" alt="gridview icon" width="20px" style="margin:0 5px;"> gridview icon will display the list in a grid, the <img src="assets/icon-listview.png?raw=true" alt="listview icon" width="20px" style="margin:0 5px;"> listview icon will arrange the items in a list. The sort icon <img src="assets/icon-sort.png?raw=true" alt="listview icon" width="20px" style="margin:0 5px;"> will open additional sort options.

View File

@@ -1,10 +0,0 @@
# Tube Archivist Home Page Functionality
This is the landing page, when you first open **Tube Archivist**. You have a few options to sort and filter that view:
- With the **Sort Order** you can select how the "Recent Videos" are sorted:
- **Date Published**: Sorts the list by date when the video was published on YouTube, newest on top.
- **Date Downloaded**: Sorts the list based on when you have downloaded the video to your archive, newest on top.
- With **Hide Watched** you can filter out videos you have already marked as watched to only show unwatched videos.
- You can use those two options together to for example filter the list to *Hide Watched* videos **and** sort by date downloaded.
Additionally the search icon <img src="assets/icon-search.png?raw=true" alt="search icon" width="20px" style="margin:0 5px;"> opens a text field to search your collection.

View File

@@ -1,5 +1,5 @@
# Playlist Overview and Playlist Detail Page
The playlists are organized in two different levels, similar as the channels:
The playlists are organized in two different levels, similar as the [channels](Channels):
## Playlist Overview
Accessible at `/playlist/` of your Tube Archivist, this **Overview Page** shows a list of all playlists you have indexed over all your channels.
@@ -13,7 +13,7 @@ The **Subscribe to Playlist** button <img src="assets/icon-add.png?raw=true" alt
- Add one per line.
- NOTE: It doesn't make sense to subscribe to a playlist if you are already subscribed the corresponding channel as this will slow down the **Rescan Subscriptions** [task](Downloads#rescan-subscriptions).
The search icon <img src="assets/icon-search.png?raw=true" alt="search icon" width="20px" style="margin:0 5px;"> opens a text box to search for indexed playlists.
You can search your indexed playlists by clicking on the search icon <img src="assets/icon-search.png?raw=true" alt="search icon" width="20px" style="margin:0 5px;">. This will open a dedicated page.
## Playlist Detail
Each playlist will get a dedicated playlist detail page accessible at `/playlist/<playlist-id>/` of your Tube Archivist. This page shows all the videos you have downloaded from this playlist.

View File

@@ -27,6 +27,10 @@ Additional settings passed to yt-dlp.
- **Embed Metadata**: This saves the available tags directly into the media file by passing `--embed-metadata` to yt-dlp.
- **Embed Thumbnail**: This will save the thumbnail into the media file by passing `--embed-thumbnail` to yt-dlp.
## Integrations
All third party integrations of TubeArchivist will **always** be *opt in*.
- **returnyoutubedislike.com**: This will get dislikes and average ratings for each video back by integarting with the API from [returnyoutubedislike.com](https://www.returnyoutubedislike.com/).
# Scheduler Setup
Schedule settings expect a cron like format, where the first value is minute, second is hour and third is day of the week. Day 0 is Sunday, day 1 is Monday etc.
@@ -40,6 +44,7 @@ Examples:
NOTE:
- Changes in the scheduler settings require a container restart to take effect.
- Cron format as *number*/*number* are none standard cron and are not supported by the scheduler, for example **0 0/12 \*** is invalid, use **0 \*/12 \*** instead.
- Avoid an unnecessary frequent schedule to not get blocked by YouTube. For that reason * or wildcards for minutes are not supported.
## Rescan Subscriptions

BIN
docs/assets/icon-sort.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -2,8 +2,16 @@ server {
listen 8000;
location /cache/ {
alias /cache/;
location /cache/videos/ {
alias /cache/videos/;
}
location /cache/channels/ {
alias /cache/channels/;
}
location /cache/playlists/ {
alias /cache/playlists/;
}
location /media/ {

4
run.sh
View File

@@ -2,6 +2,10 @@
# startup script inside the container for tubearchivist
# check environment
if [[ -z "$DJANGO_DEBUG" ]]; then
export DJANGO_DEBUG=False
fi
if [[ -z "$ELASTIC_USER" ]]; then
export ELASTIC_USER=elastic
fi

View File

@@ -7,7 +7,8 @@
"default_view": {
"home": "grid",
"channel": "list",
"downloads": "list"
"downloads": "list",
"playlist": "grid"
},
"subscriptions": {
"auto_search": false,
@@ -22,7 +23,8 @@
"format": false,
"add_metadata": false,
"add_thumbnail": false,
"throttledratelimit": false
"throttledratelimit": false,
"integrate_ryd": false
},
"application": {
"app_root": "/app",

View File

@@ -56,11 +56,18 @@ class ApplicationSettingsForm(forms.Form):
("1", "embed thumbnail"),
]
RYD_CHOICES = [
("", "-- change ryd integrations"),
("0", "disable ryd integration"),
("1", "enable ryd integration"),
]
subscriptions_channel_size = forms.IntegerField(required=False)
downloads_limit_count = forms.IntegerField(required=False)
downloads_limit_speed = forms.IntegerField(required=False)
downloads_throttledratelimit = forms.IntegerField(required=False)
downloads_sleep_interval = forms.IntegerField(required=False)
downloads_autodelete_days = forms.IntegerField(required=False)
downloads_format = forms.CharField(required=False)
downloads_add_metadata = forms.ChoiceField(
widget=forms.Select, choices=METADATA_CHOICES, required=False
@@ -68,7 +75,9 @@ class ApplicationSettingsForm(forms.Form):
downloads_add_thumbnail = forms.ChoiceField(
widget=forms.Select, choices=THUMBNAIL_CHOICES, required=False
)
downloads_autodelete_days = forms.IntegerField(required=False)
downloads_integrate_ryd = forms.ChoiceField(
widget=forms.Select, choices=RYD_CHOICES, required=False
)
class SchedulerSettingsForm(forms.Form):
@@ -83,37 +92,22 @@ class SchedulerSettingsForm(forms.Form):
run_backup_rotate = forms.IntegerField(required=False)
class VideoSearchForm(forms.Form):
"""search videos form"""
searchInput = forms.CharField(
label="Search your videos",
widget=forms.TextInput(attrs={"autocomplete": "off"}),
)
class ChannelSearchForm(forms.Form):
"""search for channels"""
class MultiSearchForm(forms.Form):
"""multi search form for /search/"""
searchInput = forms.CharField(
label="",
widget=forms.TextInput(
attrs={
"oninput": "searchChannels(this.value)",
"autocomplete": "off",
"list": "resultBox",
"oninput": "searchMulti(this.value)",
"autofocus": True,
}
),
)
class PlaylistSearchForm(forms.Form):
"""search for playlists"""
searchInput = forms.CharField(
label="",
widget=forms.TextInput(attrs={"autocomplete": "off"}),
)
home = forms.CharField(widget=forms.HiddenInput())
channel = forms.CharField(widget=forms.HiddenInput())
playlist = forms.CharField(widget=forms.HiddenInput())
class AddToQueueForm(forms.Form):
@@ -134,7 +128,7 @@ class SubscribeToChannelForm(forms.Form):
"""text area form to subscribe to multiple channels"""
subscribe = forms.CharField(
label=False,
label="Subscribe to channels",
widget=forms.Textarea(
attrs={
"rows": 3,
@@ -148,11 +142,11 @@ class SubscribeToPlaylistForm(forms.Form):
"""text area form to subscribe to multiple playlists"""
subscribe = forms.CharField(
label=False,
label="Subscribe to playlists",
widget=forms.Textarea(
attrs={
"rows": 3,
"placeholder": "Input playlist IDs",
"placeholder": "Input playlist IDs or URLs",
}
),
)

View File

@@ -7,6 +7,7 @@ Functionality:
import json
import os
import re
from celery.schedules import crontab
from home.src.helper import RedisArchivist
@@ -206,6 +207,10 @@ class ScheduleBuilder:
if to_check == "0":
# deactivate this schedule
return False
if re.search(r"[\d]{1,2}\/[\d]{1,2}", to_check):
# number/number cron format will fail in celery
print("number/number schedule formatting not supported")
raise ValueError
keys = ["minute", "hour", "day_of_week"]
if to_check == "auto":
@@ -219,8 +224,11 @@ class ScheduleBuilder:
raise ValueError("invalid input")
to_write = dict(zip(keys, values))
if "*" in to_write["minute"]:
raise ValueError("too frequent: wildcard in minutes not supported")
try:
int(to_write["minute"])
except ValueError as error:
print("too frequent: only number in minutes are supported")
raise ValueError("invalid input") from error
return to_write

View File

@@ -13,7 +13,7 @@ from datetime import datetime
from time import sleep
import requests
import yt_dlp as youtube_dl
import yt_dlp
from home.src.config import AppConfig
from home.src.helper import (
DurationConverter,
@@ -75,7 +75,7 @@ class PendingList:
return missing_videos
def add_to_pending(self, missing_videos):
def add_to_pending(self, missing_videos, ignore=False):
"""build the bulk json data from pending"""
# check if channel is indexed
channel_handler = ChannelSubscription()
@@ -84,7 +84,7 @@ class PendingList:
# check if already there
self.all_downloaded = self.get_all_downloaded()
bulk_list, all_videos_added = self.build_bulk(missing_videos)
bulk_list, all_videos_added = self.build_bulk(missing_videos, ignore)
# add last newline
bulk_list.append("\n")
query_str = "\n".join(bulk_list)
@@ -99,7 +99,7 @@ class PendingList:
return all_videos_added
def build_bulk(self, missing_videos):
def build_bulk(self, missing_videos, ignore=False):
"""build the bulk lists"""
bulk_list = []
all_videos_added = []
@@ -116,7 +116,10 @@ class PendingList:
channel_indexed = video["channel_id"] in self.all_channel_ids
video["channel_indexed"] = channel_indexed
video["status"] = "pending"
if ignore:
video["status"] = "ignore"
else:
video["status"] = "pending"
action = {"create": {"_id": youtube_id, "_index": "ta_download"}}
bulk_list.append(json.dumps(action))
bulk_list.append(json.dumps(video))
@@ -152,8 +155,8 @@ class PendingList:
"simulate": True,
}
try:
vid = youtube_dl.YoutubeDL(obs).extract_info(youtube_id)
except youtube_dl.utils.DownloadError:
vid = yt_dlp.YoutubeDL(obs).extract_info(youtube_id)
except yt_dlp.utils.DownloadError:
print("failed to extract info for: " + youtube_id)
return False
# stop if video is streaming live now
@@ -306,7 +309,7 @@ class ChannelSubscription:
}
if limit:
obs["playlistend"] = self.channel_size
chan = youtube_dl.YoutubeDL(obs).extract_info(url, download=False)
chan = yt_dlp.YoutubeDL(obs).extract_info(url, download=False)
last_videos = [(i["id"], i["title"]) for i in chan["entries"]]
return last_videos
@@ -548,7 +551,7 @@ class VideoDownloader:
try:
self.dl_single_vid(youtube_id)
except youtube_dl.utils.DownloadError:
except yt_dlp.utils.DownloadError:
print("failed to download " + youtube_id)
continue
vid_dict = index_new_video(youtube_id)
@@ -590,7 +593,7 @@ class VideoDownloader:
@staticmethod
def progress_hook(response):
"""process the progress_hooks from youtube_dl"""
"""process the progress_hooks from yt_dlp"""
# title
path = os.path.split(response["filename"])[-1][12:]
filename = os.path.splitext(os.path.splitext(path)[0])[0]
@@ -642,10 +645,6 @@ class VideoDownloader:
if throttle:
obs["throttledratelimit"] = throttle * 1024
external = False
if external:
obs["external_downloader"] = "aria2c"
postprocessors = []
if self.config["downloads"]["add_metadata"]:
@@ -680,10 +679,10 @@ class VideoDownloader:
for file_name in all_cached:
if youtube_id in file_name:
obs["outtmpl"] = os.path.join(dl_cache, file_name)
with youtube_dl.YoutubeDL(obs) as ydl:
with yt_dlp.YoutubeDL(obs) as ydl:
try:
ydl.download([youtube_id])
except youtube_dl.utils.DownloadError:
except yt_dlp.utils.DownloadError:
print("retry failed download: " + youtube_id)
sleep(10)
ydl.download([youtube_id])
@@ -790,8 +789,14 @@ class VideoDownloader:
"sort": [{"player.watched_date": {"order": "asc"}}],
}
all_to_delete = IndexPaginate("ta_video", data).get_results()
all_youtube_ids = [i["youtube_id"] for i in all_to_delete]
if not all_youtube_ids:
return
for to_delete in all_to_delete:
youtube_id = to_delete["youtube_id"]
for youtube_id in all_youtube_ids:
print(f"autodelete {youtube_id}")
YoutubeVideo(youtube_id).delete_media_file()
print("add deleted to ignore list")
pending_handler = PendingList()
pending_handler.add_to_pending(all_youtube_ids, ignore=True)

View File

@@ -75,7 +75,7 @@ class PostData:
"db-backup": self._db_backup,
"db-restore": self._db_restore,
"fs-rescan": self._fs_rescan,
"channel-search": self._channel_search,
"multi_search": self._multi_search,
"delete-video": self._delete_video,
"delete-channel": self._delete_channel,
"delete-playlist": self._delete_playlist,
@@ -282,11 +282,11 @@ class PostData:
rescan_filesystem.delay()
return {"success": True}
def _channel_search(self):
"""search for channel name as_you_type"""
def _multi_search(self):
"""search through all indexes"""
search_query = self.exec_val
print("searching for: " + search_query)
search_results = SearchForm().search_channels(search_query)
search_results = SearchForm().multi_search(search_query)
return search_results
def _delete_video(self):

View File

@@ -13,7 +13,7 @@ from urllib.parse import parse_qs, urlparse
import redis
import requests
import yt_dlp as youtube_dl
import yt_dlp
def get_total_hits(index, es_url, es_auth, match_field):
@@ -139,7 +139,7 @@ class UrlListParser:
"extract_flat": True,
"playlistend": 0,
}
url_info = youtube_dl.YoutubeDL(obs).extract_info(url, download=False)
url_info = yt_dlp.YoutubeDL(obs).extract_info(url, download=False)
try:
channel_id = url_info["channel_id"]
except KeyError as error:

View File

@@ -12,11 +12,12 @@ from datetime import datetime
from time import sleep
import requests
import yt_dlp as youtube_dl
import yt_dlp
from bs4 import BeautifulSoup
from home.src.config import AppConfig
from home.src.helper import DurationConverter, UrlListParser, clean_string
from home.src.thumbnails import ThumbManager
from ryd_client import ryd_client
class YoutubeChannel:
@@ -223,6 +224,13 @@ class YoutubeChannel:
if not response.ok:
print(response.text)
def delete_playlists(self):
"""delete all indexed playlist from es"""
all_playlists = self.get_indexed_playlists()
for playlist in all_playlists:
playlist_id = playlist["playlist_id"]
YoutubePlaylist(playlist_id).delete_metadata()
def delete_channel(self):
"""delete channel and all videos"""
print(f"deleting {self.channel_id} and all matching media files")
@@ -238,7 +246,8 @@ class YoutubeChannel:
print(f"no videos found for {folder_path}")
ThumbManager().delete_chan_thumb(self.channel_id)
print("delete indexed playlists")
self.delete_playlists()
print("delete indexed videos")
self.delete_es_videos()
url = self.ES_URL + "/ta_channel/_doc/" + self.channel_id
@@ -257,7 +266,7 @@ class YoutubeChannel:
"skip_download": True,
"extract_flat": True,
}
playlists = youtube_dl.YoutubeDL(obs).extract_info(url)
playlists = yt_dlp.YoutubeDL(obs).extract_info(url)
all_entries = [(i["id"], i["title"]) for i in playlists["entries"]]
return all_entries
@@ -289,7 +298,7 @@ class YoutubeVideo:
self.vid_dict = None
def get_vid_dict(self):
"""wrapper to loop around youtube_dl to retry on failure"""
"""wrapper to loop around yt_dlp to retry on failure"""
print(f"get video data for {self.youtube_id}")
vid_dict = False
for i in range(3):
@@ -303,6 +312,8 @@ class YoutubeVideo:
break
self.vid_dict = vid_dict
if self.CONFIG["downloads"]["integrate_ryd"]:
self.get_ryd_stats()
def get_youtubedl_vid_data(self):
"""parse youtubedl extract info"""
@@ -315,10 +326,10 @@ class YoutubeVideo:
"noplaylist": True,
}
try:
vid = youtube_dl.YoutubeDL(obs).extract_info(youtube_id)
vid = yt_dlp.YoutubeDL(obs).extract_info(youtube_id)
except (
youtube_dl.utils.ExtractorError,
youtube_dl.utils.DownloadError,
yt_dlp.utils.ExtractorError,
yt_dlp.utils.DownloadError,
):
print("failed to get info for " + youtube_id)
return False
@@ -446,6 +457,23 @@ class YoutubeVideo:
# delete thumbs from cache
ThumbManager().delete_vid_thumb(self.youtube_id)
def get_ryd_stats(self):
"""get optional stats from returnyoutubedislikeapi.com"""
try:
print(f"get ryd stats for: {self.youtube_id}")
result = ryd_client.get(self.youtube_id)
except requests.exceptions.ConnectionError:
print(f"failed to query ryd api, skipping {self.youtube_id}")
return False
dislikes = {
"dislike_count": result["dislikes"],
"average_rating": result["rating"],
}
self.vid_dict["stats"].update(dislikes)
return True
class YoutubePlaylist:
"""represent a single playlist on YouTube"""
@@ -489,12 +517,10 @@ class YoutubePlaylist:
"playlistend": 0,
}
try:
playlist = youtube_dl.YoutubeDL(obs).extract_info(
url, download=False
)
playlist = yt_dlp.YoutubeDL(obs).extract_info(url, download=False)
except (
youtube_dl.utils.ExtractorError,
youtube_dl.utils.DownloadError,
yt_dlp.utils.ExtractorError,
yt_dlp.utils.DownloadError,
):
print("failed to get info for " + self.playlist_id)
return False
@@ -535,12 +561,10 @@ class YoutubePlaylist:
obs["playlistend"] = playlistend
try:
playlist = youtube_dl.YoutubeDL(obs).extract_info(
url, download=False
)
playlist = yt_dlp.YoutubeDL(obs).extract_info(url, download=False)
except (
youtube_dl.utils.ExtractorError,
youtube_dl.utils.DownloadError,
yt_dlp.utils.ExtractorError,
yt_dlp.utils.DownloadError,
):
print("failed to get plealist entries for " + self.playlist_id)
return False

View File

@@ -25,6 +25,7 @@ INDEX_CONFIG = [
},
"channel_name": {
"type": "text",
"analyzer": "english",
"fields": {
"keyword": {
"type": "keyword",
@@ -64,6 +65,7 @@ INDEX_CONFIG = [
},
"channel_name": {
"type": "text",
"analyzer": "english",
"fields": {
"keyword": {
"type": "keyword",
@@ -88,8 +90,16 @@ INDEX_CONFIG = [
},
"description": {"type": "text"},
"media_url": {"type": "keyword", "index": False},
"tags": {
"type": "text",
"analyzer": "english",
"fields": {
"keyword": {"type": "keyword", "ignore_above": 256}
},
},
"title": {
"type": "text",
"analyzer": "english",
"fields": {
"keyword": {
"type": "keyword",
@@ -171,12 +181,18 @@ INDEX_CONFIG = [
"playlist_description": {"type": "text"},
"playlist_name": {
"type": "text",
"analyzer": "english",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256,
"normalizer": "to_lower",
}
},
"search_as_you_type": {
"type": "search_as_you_type",
"doc_values": False,
"max_shingle_size": 3,
},
},
},
"playlist_channel": {

View File

@@ -42,6 +42,7 @@ class Reindex:
self.es_url = config["application"]["es_url"]
self.es_auth = config["application"]["es_auth"]
self.refresh_interval = config["scheduler"]["check_reindex_days"]
self.integrate_ryd = config["downloads"]["integrate_ryd"]
# scan
self.all_youtube_ids = False
self.all_channel_ids = False
@@ -92,6 +93,28 @@ class Reindex:
all_youtube_ids = [i["_id"] for i in response_dict["hits"]["hits"]]
return all_youtube_ids
def get_unrated_vids(self):
"""get all videos without rating if ryd integration is enabled"""
headers = {"Content-type": "application/json"}
data = {
"size": 200,
"query": {
"bool": {
"must_not": [{"exists": {"field": "stats.average_rating"}}]
}
},
}
query_str = json.dumps(data)
url = self.es_url + "/ta_video/_search"
response = requests.get(
url, data=query_str, headers=headers, auth=self.es_auth
)
if not response.ok:
print(response.text)
response_dict = json.loads(response.text)
missing_rating = [i["_id"] for i in response_dict["hits"]["hits"]]
self.all_youtube_ids = self.all_youtube_ids + missing_rating
def get_outdated_channels(self, size):
"""get daily channels to refresh"""
headers = {"Content-type": "application/json"}
@@ -156,6 +179,8 @@ class Reindex:
self.all_youtube_ids = self.get_outdated_vids(video_daily)
self.all_channel_ids = self.get_outdated_channels(channel_daily)
self.all_playlist_ids = self.get_outdated_playlists(playlist_daily)
if self.integrate_ryd:
self.get_unrated_vids()
def rescrape_all_channels(self):
"""sync new data from channel to all matching videos"""
@@ -566,14 +591,10 @@ def scan_filesystem():
def reindex_old_documents():
"""daily refresh of old documents"""
# check needed last run
now = int(datetime.now().strftime("%s"))
last_reindex = RedisArchivist().get_message("last_reindex")
if isinstance(last_reindex, int) and now - last_reindex < 60 * 60 * 24:
return
# continue if needed
reindex_handler = Reindex()
reindex_handler.check_outdated()
reindex_handler.reindex()
# set timestamp
now = int(datetime.now().strftime("%s"))
RedisArchivist().set_message("last_reindex", now, expire=False)

View File

@@ -155,32 +155,63 @@ class SearchForm:
CONFIG = AppConfig().config
ES_URL = CONFIG["application"]["es_url"]
def search_channels(self, search_query):
"""fancy searching channels as you type"""
url = self.ES_URL + "/ta_channel/_search"
def multi_search(self, search_query):
"""searching through index"""
url = self.ES_URL + "/ta_video,ta_channel,ta_playlist/_search"
data = {
"size": 10,
"size": 30,
"query": {
"multi_match": {
"query": search_query,
"type": "bool_prefix",
"operator": "and",
"fuzziness": "auto",
"fields": [
"channel_name.search_as_you_type",
"category",
"channel_description",
"channel_name._2gram",
"channel_name._3gram",
"channel_name.search_as_you_type",
"playlist_description",
"playlist_name._2gram",
"playlist_name._3gram",
"playlist_name.search_as_you_type",
"tags",
"title._2gram",
"title._3gram",
"title.search_as_you_type",
],
}
},
}
look_up = SearchHandler(url, data)
search_results = look_up.get_data()
return {"results": search_results}
all_results = self.build_results(search_results)
return {"results": all_results}
@staticmethod
def search_videos():
"""searching for videos"""
# TBD palceholder for now
return False
def build_results(search_results):
"""build the all_results dict"""
video_results = []
channel_results = []
playlist_results = []
if search_results:
for result in search_results:
if result["_index"] == "ta_video":
video_results.append(result)
elif result["_index"] == "ta_channel":
channel_results.append(result)
elif result["_index"] == "ta_playlist":
playlist_results.append(result)
all_results = {
"video_results": video_results,
"channel_results": channel_results,
"playlist_results": playlist_results,
}
return all_results
class Pagination:

View File

@@ -106,6 +106,13 @@ def download_single(youtube_id):
try:
have_lock = my_lock.acquire(blocking=False)
if have_lock:
mess_dict = {
"status": "message:download",
"level": "info",
"title": "Download single video",
"message": "processing",
}
RedisArchivist().set_message("message:download", mess_dict)
VideoDownloader().run_queue()
else:
print("Download queue already running.")

View File

@@ -58,8 +58,8 @@
</a>
</div>
<div class="nav-icons">
<a href="{% url 'about' %}">
<img src="{% static 'img/icon-help.svg' %}" alt="help-icon" title="About">
<a href="{% url 'search' %}">
<img src="{% static 'img/icon-search.svg' %}" alt="search-icon" title="Search">
</a>
<a href="{% url 'settings' %}">
<img src="{% static 'img/icon-gear.svg' %}" alt="gear-icon" title="Settings">
@@ -116,7 +116,7 @@
</div>
<div class="footer">
<div class="boxed-content">
<span>© 2021 - <script type="text/javascript">document.write(new Date().getFullYear());</script> The Tube Archivist v0.0.9 | <a href="https://github.com/bbilly1/tubearchivist" target="_blank">Github</a> | <a href="https://hub.docker.com/r/bbilly1/tubearchivist" target="_blank">Docker Hub</a></span>
<span>© 2021 - <script type="text/javascript">document.write(new Date().getFullYear());</script> TubeArchivist v0.1.0 </span><span><a href="{% url 'about' %}">About</a> | <a href="https://github.com/bbilly1/tubearchivist" target="_blank">GitHub</a> | <a href="https://hub.docker.com/r/bbilly1/tubearchivist" target="_blank">Docker Hub</a> | <a href="https://discord.gg/AFwz8nE7BK" target="_blank">Discord</a> | <a href="https://www.reddit.com/r/TubeArchivist/">Reddit</a></span>
</div>
</div>
</body>

View File

@@ -3,14 +3,12 @@
{% load humanize %}
{% block content %}
<div class="boxed-content">
<div class="title-bar">
<h1>Channels</h1>
</div>
<div id="notifications" data="channel"></div>
<div class="info-box info-box-2">
<div class="icon-text">
<img id="add-icon" onclick="showForm()" src="{% static 'img/icon-add.svg' %}" alt="add-icon">
<p>Subscribe to Channels</p>
<div class="title-split">
<div class="title-bar">
<h1>Channels</h1>
</div>
<div class="title-split-form">
<img id="animate-icon" onclick="showForm()" src="{% static 'img/icon-add.svg' %}" alt="add-icon" title="Subscribe to Channels">
<div class="show-form">
<form id="hidden-form" action="/channel/" method="post">
{% csrf_token %}
@@ -19,31 +17,18 @@
</form>
</div>
</div>
<div class="search-form icon-text">
<div class="search-icon">
<img src="{% static 'img/icon-search.svg' %}" alt="search-icon" onclick="showSearch()">
<p>Search your Channels</p>
</div>
<form onSubmit="return channelRedirect();" id="search-box">
{% csrf_token %}
{{ search_form }}
<datalist id="resultBox">
</datalist>
</form>
</div>
</div>
<div id="notifications" data="channel"></div>
<div class="view-controls">
<div class="toggle">
<span>Show only subscribed channels:</span>
<span>Show subscribed only:</span>
<div class="toggleBox">
<input
id="show_subed_only" onclick="toggleCheckbox(this)" type="checkbox"
{% if show_subed_only %}
checked
{% endif %}
>
<label for="" class="onbtn">On</label>
<label for="" class="ofbtn">Off</label>
<input id="show_subed_only" onclick="toggleCheckbox(this)" type="checkbox" {% if show_subed_only %}checked{% endif %}>
{% if show_subed_only %}
<label for="" class="ofbtn">Off</label>
{% else %}
<label for="" class="onbtn">On</label>
{% endif %}
</div>
</div>
<div class="view-icons">
@@ -53,8 +38,8 @@
</div>
<h2>Total matching channels: {{ max_hits }}</h2>
<div class="channel-list {{ view_style }}">
{% if channels %}
{% for channel in channels %}
{% if results %}
{% for channel in results %}
<div class="channel-item {{ view_style }}">
<div class="channel-banner {{ view_style }}">
<a href="{% url 'channel_id' channel.source.channel_id %}">
@@ -80,11 +65,11 @@
<div class="info-box-item">
<div>
<p>Last refreshed: {{ channel.source.channel_last_refresh }}</p>
<p>{% if channel.source.channel_subscribed %}
{% if channel.source.channel_subscribed %}
<button class="unsubscribe" type="button" id="{{ channel.source.channel_id }}" onclick="unsubscribe(this.id)" title="Unsubscribe from {{ channel.source.channel_name }}">Unsubscribe</button>
{% else %}
<button type="button" id="{{ channel.source.channel_id }}" onclick="subscribe(this.id)" title="Subscribe to {{ channel.source.channel_name }}">Subscribe</button>
{% endif %}</p>
{% endif %}
</div>
</div>
</div>

View File

@@ -66,54 +66,45 @@
</div>
{% endif %}
<div id="notifications" data="channel_id"></div>
</div>
<div id="player" class="player-wrapper"></div>
<div class="boxed-content">
<div class="sort">
<p>Sort by <span class="settings-current">{{ sort_by }}</span>
<select name="sort" id="sort" onchange="sortChange(this.value)">
<option value="" disabled selected> -- change sort by -- </option>
<option value="published">date published</option>
<option value="downloaded">date downloaded</option>
<option value="views">views</option>
<option value="likes">likes</option>
</select>
<select name="sord-order" id="sort-order" onchange="sortChange(this.value)">
{% if sort_order == "asc" %}
<option value="asc" selected>asc</option>
{% else %}
<option value="asc">asc</option>
{% endif %}
{% if sort_order == "desc" %}
<option value="desc" selected>desc</option>
{% else %}
<option value="desc">desc</option>
{% endif %}
</select>
</p>
</div>
<div class="view-controls">
<div class="toggle">
<span>Hide watched videos:</span>
<div class="toggleBox">
<input
id="hide_watched" onclick="toggleCheckbox(this)" type="checkbox"
{% if hide_watched %}
checked
{% endif %}
>
<label for="" class="onbtn">On</label>
<label for="" class="ofbtn">Off</label>
<input id="hide_watched" onclick="toggleCheckbox(this)" type="checkbox" {% if hide_watched %}checked{% endif %}>
{% if hide_watched %}
<label for="" class="ofbtn">Off</label>
{% else %}
<label for="" class="onbtn">On</label>
{% endif %}
</div>
</div>
<div class="sort">
<div id="hidden-form">
<span>Sort by:</span>
<select name="sort" id="sort" onchange="sortChange(this.value)">
<option value="published" {% if sort_by == "published" %}selected{% endif %}>date published</option>
<option value="downloaded" {% if sort_by == "downloaded" %}selected{% endif %}>date downloaded</option>
<option value="views" {% if sort_by == "views" %}selected{% endif %}>views</option>
<option value="likes" {% if sort_by == "likes" %}selected{% endif %}>likes</option>
</select>
<select name="sord-order" id="sort-order" onchange="sortChange(this.value)">
<option value="asc" {% if sort_order == "asc" %}selected{% endif %}>asc</option>
<option value="desc" {% if sort_order == "desc" %}selected{% endif %}>desc</option>
</select>
</div>
</div>
<div class="view-icons">
<img src="{% static 'img/icon-sort.svg' %}" alt="sort-icon" onclick="showForm()" id="animate-icon">
<img src="{% static 'img/icon-gridview.svg' %}" onclick="changeView(this)" data-origin="home" data-value="grid" alt="grid view">
<img src="{% static 'img/icon-listview.svg' %}" onclick="changeView(this)" data-origin="home" data-value="list" alt="list view">
</div>
</div>
</div>
<div id="player" class="player-wrapper"></div>
<div class="boxed-content">
<div class="video-list {{ view_style }}">
{% if videos %}
{% for video in videos %}
{% if results %}
{% for video in results %}
<div class="video-item {{ view_style }}">
<a href="#player" data-src="/media/{{ video.source.media_url }}" data-thumb="/cache/{{ video.source.vid_thumb_url }}" data-title="{{ video.source.title }}" data-channel="{{ video.source.channel.channel_name }}" data-channel-id="{{ video.source.channel.channel_id }}" data-id="{{ video.source.youtube_id }}" onclick="createPlayer(this)">
<div class="video-thumb-wrap {{ view_style }}">

View File

@@ -17,7 +17,7 @@
<p>Start download</p>
</div>
<div class="icon-text">
<img id="add-icon" onclick="showForm()" src="{% static 'img/icon-add.svg' %}" alt="add-icon">
<img id="animate-icon" onclick="showForm()" src="{% static 'img/icon-add.svg' %}" alt="add-icon">
<p>Add to download queue</p>
<div class="show-form">
<form id='hidden-form' action="/downloads/" method="post">
@@ -32,14 +32,12 @@
<div class="toggle">
<span>Show only ignored videos:</span>
<div class="toggleBox">
<input
id="show_ignored_only" onclick="toggleCheckbox(this)" type="checkbox"
{% if show_ignored_only %}
checked
{% endif %}
>
<label for="" class="onbtn">On</label>
<label for="" class="ofbtn">Off</label>
<input id="show_ignored_only" onclick="toggleCheckbox(this)" type="checkbox" {% if show_ignored_only %}checked{% endif %}>
{% if show_ignored_only %}
<label for="" class="ofbtn">Off</label>
{% else %}
<label for="" class="onbtn">On</label>
{% endif %}
</div>
</div>
<div class="view-icons">
@@ -58,11 +56,11 @@
</div>
<h3>Total videos: {{ max_hits }}</h3>
<div class="dl-list {{ view_style }}">
{% if all_video_hits %}
{% for video in all_video_hits %}
<div class="dl-item {{ view_style }}" id="dl-{{ video.youtube_id }}">
{% if results %}
{% for video in results %}
<div class="dl-item {{ view_style }}" id="dl-{{ video.source.youtube_id }}">
<div class="dl-thumb {{ view_style }}">
<img src="/cache/{{ video.vid_thumb_url }}" alt="video_thumb">
<img src="/cache/{{ video.source.vid_thumb_url }}" alt="video_thumb">
{% if show_ignored_only %}
<span>ignored</span>
{% else %}
@@ -70,19 +68,19 @@
{% endif %}
</div>
<div class="dl-desc {{ view_style }}">
<h3>{{ video.title }}</h3>
{% if video.channel_indexed %}
<a href="{% url 'channel_id' video.channel_id %}">{{ video.channel_name }}</a>
<h3>{{ video.source.title }}</h3>
{% if video.source.channel_indexed %}
<a href="{% url 'channel_id' video.source.channel_id %}">{{ video.source.channel_name }}</a>
{% else %}
<span>{{ video.channel_name }}</span>
<span>{{ video.source.channel_name }}</span>
{% endif %}
<p>Published: {{ video.published }} | Duration: {{ video.duration }} | {{ video.youtube_id }}</p>
<p>Published: {{ video.source.published }} | Duration: {{ video.source.duration }} | {{ video.source.youtube_id }}</p>
{% if show_ignored_only %}
<button data-id="{{ video.youtube_id }}" onclick="forgetIgnore(this)">Forget</button>
<button data-id="{{ video.youtube_id }}" onclick="addSingle(this)">Add to queue</button>
<button data-id="{{ video.source.youtube_id }}" onclick="forgetIgnore(this)">Forget</button>
<button data-id="{{ video.source.youtube_id }}" onclick="addSingle(this)">Add to queue</button>
{% else %}
<button data-id="{{ video.youtube_id }}" onclick="toIgnore(this)">Ignore</button>
<button id="{{ video.youtube_id }}" data-id="{{ video.youtube_id }}" onclick="downloadNow(this)">Download now</button>
<button data-id="{{ video.source.youtube_id }}" onclick="toIgnore(this)">Ignore</button>
<button id="{{ video.source.youtube_id }}" data-id="{{ video.source.youtube_id }}" onclick="downloadNow(this)">Download now</button>
{% endif %}
</div>
</div>

View File

@@ -5,55 +5,35 @@
<div class="title-bar">
<h1>Recent Videos</h1>
</div>
<div class="info-box info-box-2">
<div class="sort">
<p>Sort by <span class="settings-current">{{ sort_by }}</span>
<select name="sort" id="sort" onchange="sortChange(this.value)">
<option value="" disabled selected> -- change sort by -- </option>
<option value="published">date published</option>
<option value="downloaded">date downloaded</option>
<option value="views">views</option>
<option value="likes">likes</option>
</select>
<select name="sord-order" id="sort-order" onchange="sortChange(this.value)">
{% if sort_order == "asc" %}
<option value="asc" selected>asc</option>
{% else %}
<option value="asc">asc</option>
{% endif %}
{% if sort_order == "desc" %}
<option value="desc" selected>desc</option>
{% else %}
<option value="desc">desc</option>
{% endif %}
</select>
</p>
</div>
<div class="search-form icon-text">
<div class="search-icon">
<img src="{% static 'img/icon-search.svg' %}" alt="search-icon" onclick="showSearch()">
</div>
<form action="/" method="POST" id="search-box">
{% csrf_token %}
{{ search_form }}
</form>
</div>
</div>
<div class="view-controls">
<div class="toggle">
<span>Hide watched videos:</span>
<span>Hide watched:</span>
<div class="toggleBox">
<input
id="hide_watched" onclick="toggleCheckbox(this)" type="checkbox"
{% if hide_watched %}
checked
{% endif %}
>
<label for="" class="onbtn">On</label>
<label for="" class="ofbtn">Off</label>
<input id="hide_watched" onclick="toggleCheckbox(this)" type="checkbox" {% if hide_watched %}checked{% endif %}>
{% if hide_watched %}
<label for="" class="ofbtn">Off</label>
{% else %}
<label for="" class="onbtn">On</label>
{% endif %}
</div>
</div>
<div class="sort">
<div id="hidden-form">
<span>Sort by:</span>
<select name="sort" id="sort" onchange="sortChange(this.value)">
<option value="published" {% if sort_by == "published" %}selected{% endif %}>date published</option>
<option value="downloaded" {% if sort_by == "downloaded" %}selected{% endif %}>date downloaded</option>
<option value="views" {% if sort_by == "views" %}selected{% endif %}>views</option>
<option value="likes" {% if sort_by == "likes" %}selected{% endif %}>likes</option>
</select>
<select name="sord-order" id="sort-order" onchange="sortChange(this.value)">
<option value="asc" {% if sort_order == "asc" %}selected{% endif %}>asc</option>
<option value="desc" {% if sort_order == "desc" %}selected{% endif %}>desc</option>
</select>
</div>
</div>
<div class="view-icons">
<img src="{% static 'img/icon-sort.svg' %}" alt="sort-icon" onclick="showForm()" id="animate-icon">
<img src="{% static 'img/icon-gridview.svg' %}" onclick="changeView(this)" data-origin="home" data-value="grid" alt="grid view">
<img src="{% static 'img/icon-listview.svg' %}" onclick="changeView(this)" data-origin="home" data-value="list" alt="list view">
</div>
@@ -62,8 +42,8 @@
<div id="player" class="player-wrapper"></div>
<div class="boxed-content">
<div class="video-list {{ view_style }}">
{% if videos %}
{% for video in videos %}
{% if results %}
{% for video in results %}
<div class="video-item {{ view_style }}">
<a href="#player" data-src="/media/{{ video.source.media_url }}" data-thumb="/cache/{{ video.source.vid_thumb_url }}" data-title="{{ video.source.title }}" data-channel="{{ video.source.channel.channel_name }}" data-channel-id="{{ video.source.channel.channel_id }}" data-id="{{ video.source.youtube_id }}" onclick="createPlayer(this)">
<div class="video-thumb-wrap {{ view_style }}">

View File

@@ -7,7 +7,17 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TA | Welcome</title>
<link rel="stylesheet" type="text/css" href="{% static 'css/style.css' %}">
<link rel="shortcut icon" type="image/png" href="{% static 'favicon.ico' %}"/>
<link rel="apple-touch-icon" sizes="180x180" href="{% static 'favicon/apple-touch-icon.png' %}">
<link rel="icon" type="image/png" sizes="32x32" href="{% static 'favicon/favicon-32x32.png' %}">
<link rel="icon" type="image/png" sizes="16x16" href="{% static 'favicon/favicon-16x16.png' %}">
<link rel="manifest" href="{% static 'favicon/site.webmanifest' %}">
<link rel="mask-icon" href="{% static 'favicon/safari-pinned-tab.svg' %}" color="#01202e">
<link rel="shortcut icon" href="{% static 'favicon/favicon.ico' %}">
<meta name="apple-mobile-web-app-title" content="TubeArchivist">
<meta name="application-name" content="TubeArchivist">
<meta name="msapplication-TileColor" content="#01202e">
<meta name="msapplication-config" content="{% static 'favicon/browserconfig.xml' %}">
<meta name="theme-color" content="#01202e">
{% if colors == "dark" %}
<link rel="stylesheet" href="{% static 'css/dark.css' %}">
{% else %}

View File

@@ -2,49 +2,32 @@
{% load static %}
{% block content %}
<div class="boxed-content">
<div class="title-bar">
<h1>Playlists</h1>
<div class="title-split">
<div class="title-bar">
<h1>Playlists</h1>
</div>
<div class="title-split-form">
<img id="animate-icon" onclick="showForm()" 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>
</div>
</div>
</div>
<div id="notifications" data="playlist"></div>
<div class="info-box info-box-2">
<div class="icon-text">
{% if running == "subscribing" %}
<p>Subscribing in progress, refresh.</p>
{% else %}
<img id="add-icon" onclick="showForm()" src="{% static 'img/icon-add.svg' %}" alt="add-icon">
<p>Subscribe to Playlist</p>
<div class="show-form">
<form id="hidden-form" action="/playlist/" method="post">
{% csrf_token %}
{{ subscribe_form }}
<button type="submit">Subscribe</button>
</form>
</div>
{% endif %}
</div>
<div class="search-form icon-text">
<div class="search-icon">
<img src="{% static 'img/icon-search.svg' %}" alt="search-icon" onclick="showSearch()">
<p>Search your Playlists</p>
</div>
<form action="/playlist/" method="POST" id="search-box">
{% csrf_token %}
{{ search_form }}
</form>
</div>
</div>
<div class="view-controls">
<div class="toggle">
<span>Show only subscribed playlists:</span>
<span>Show subscribed only:</span>
<div class="toggleBox">
<input
id="show_subed_only" onclick="toggleCheckbox(this)" type="checkbox"
{% if show_subed_only %}
checked
{% endif %}
>
<label for="" class="onbtn">On</label>
<label for="" class="ofbtn">Off</label>
<input id="show_subed_only" onclick="toggleCheckbox(this)" type="checkbox" {% if show_subed_only %}checked{% endif %}>
{% if show_subed_only %}
<label for="" class="ofbtn">Off</label>
{% else %}
<label for="" class="onbtn">On</label>
{% endif %}
</div>
</div>
<div class="view-icons">
@@ -53,8 +36,8 @@
</div>
</div>
<div class="playlist-list {{ view_style }}">
{% if playlists %}
{% for playlist in playlists %}
{% if results %}
{% for playlist in results %}
<div class="playlist-item {{ view_style }}">
<div class="playlist-thumbnail">
<a href="{% url 'playlist_id' playlist.source.playlist_id %}">
@@ -65,11 +48,11 @@
<a href="{% url 'channel_id' playlist.source.playlist_channel_id %}"><h3>{{ playlist.source.playlist_channel }}</h3></a>
<a href="{% url 'playlist_id' playlist.source.playlist_id %}"><h2>{{ playlist.source.playlist_name }}</h2></a>
<p>Last refreshed: {{ playlist.source.playlist_last_refresh }}</p>
<p>{% if playlist.source.playlist_subscribed %}
{% if playlist.source.playlist_subscribed %}
<button class="unsubscribe" type="button" id="{{ playlist.source.playlist_id }}" onclick="unsubscribe(this.id)" title="Unsubscribe from {{ playlist.source.playlist_name }}">Unsubscribe</button>
{% else %}
<button type="button" id="{{ playlist.source.playlist_id }}" onclick="subscribe(this.id)" title="Subscribe to {{ playlist.source.playlist_name }}">Subscribe</button>
{% endif %}</p>
{% endif %}
</div>
</div>
{% endfor %}

View File

@@ -25,7 +25,7 @@
<div class="info-box-item">
<div>
<p>Last refreshed: {{ playlist_info.playlist_last_refresh }}</p>
<p>Playlist:
<p>Playlist:
{% if playlist_info.playlist_subscribed %}
<button class="unsubscribe" type="button" id="{{ playlist_info.playlist_id }}" onclick="unsubscribe(this.id)" title="Unsubscribe from {{ playlist_info.playlist_name }}">Unsubscribe</button>
{% else %}
@@ -63,17 +63,16 @@
</div>
</div>
{% endif %}
</div>
<div id="player" class="player-wrapper"></div>
<div class="boxed-content">
<div class="view-controls">
<div class="toggle">
<span>Hide watched videos:</span>
<div class="toggleBox">
<input id="hide_watched" onclick="toggleCheckbox(this)" type="checkbox"
{% if hide_watched %}checked{% endif %}>
<label for="" class="onbtn">On</label>
<label for="" class="ofbtn">Off</label>
<input id="hide_watched" onclick="toggleCheckbox(this)" type="checkbox" {% if hide_watched %}checked{% endif %}>
{% if hide_watched %}
<label for="" class="ofbtn">Off</label>
{% else %}
<label for="" class="onbtn">On</label>
{% endif %}
</div>
</div>
<div class="view-icons">
@@ -81,9 +80,12 @@
<img src="{% static 'img/icon-listview.svg' %}" onclick="changeView(this)" data-origin="home" data-value="list" alt="list view">
</div>
</div>
</div>
<div id="player" class="player-wrapper"></div>
<div class="boxed-content">
<div class="video-list {{ view_style }}">
{% if videos %}
{% for video in videos %}
{% if results %}
{% for video in results %}
<div class="video-item {{ view_style }}">
<a href="#player" data-src="/media/{{ video.source.media_url }}" data-thumb="/cache/{{ video.source.vid_thumb_url }}" data-title="{{ video.source.title }}" data-channel="{{ video.source.channel.channel_name }}" data-channel-id="{{ video.source.channel.channel_id }}" data-id="{{ video.source.youtube_id }}" onclick="createPlayer(this)">
<div class="video-thumb-wrap {{ view_style }}">

View File

@@ -0,0 +1,30 @@
{% extends "home/base.html" %}
{% block content %}
<div id="player" class="player-wrapper"></div>
<div class="boxed-content">
<div class="title-bar">
<h1>Search</h1>
</div>
<div class="multi-search-box">
{{ search_form }}
</div>
<div class="multi-search-result">
<h2>Video Results</h2>
<div id="video-results" class="video-list {{ all_styles.home }}">
<p>No videos found.</p>
</div>
</div>
<div class="multi-search-result">
<h2>Channel Results</h2>
<div id="channel-results" class="channel-list {{ all_styles.channel }}">
<p>No channels found.</p>
</div>
</div>
<div class="multi-search-result">
<h2>Playlist Results</h2>
<div id="playlist-results" class="playlist-list {{ all_styles.playlist }}">
<p>No playlists found.</p>
</div>
</div>
</div>
{% endblock content %}

View File

@@ -95,6 +95,14 @@
{{ app_form.downloads_add_thumbnail }}
</div>
</div>
<div class="settings-group">
<h2 id="integrations">Integrations</h2>
<div class="settings-item">
<p>Integrate with <a href="https://returnyoutubedislike.com/">returnyoutubedislike.com</a> to get dislikes and average ratings back: <span class="settings-current">{{ config.downloads.integrate_ryd }}</span></p>
<i>Before activating that, make sure you have a scraping sleep interval of at least 3 secs set to avoid ratelimiting issues.</i><br>
{{ app_form.downloads_integrate_ryd }}
</div>
</div>
<button type="submit" name="application-settings">Update Application Configurations</button>
</form>
<div class="title-bar">

View File

@@ -13,6 +13,7 @@ from home.views import (
LoginView,
PlaylistIdView,
PlaylistView,
SearchView,
SettingsView,
VideoView,
process,
@@ -37,7 +38,7 @@ urlpatterns = [
path("progress/", login_required(progress), name="progress"),
path("channel/", login_required(ChannelView.as_view()), name="channel"),
path(
"channel/<slug:channel_id_detail>/",
"channel/<slug:channel_id>/",
login_required(ChannelIdView.as_view()),
name="channel_id",
),
@@ -48,8 +49,9 @@ urlpatterns = [
),
path("playlist/", login_required(PlaylistView.as_view()), name="playlist"),
path(
"playlist/<slug:playlist_id_detail>/",
"playlist/<slug:playlist_id>/",
login_required(PlaylistIdView.as_view()),
name="playlist_id",
),
path("search/", login_required(SearchView.as_view()), name="search"),
]

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,10 @@
beautifulsoup4==4.10.0
celery==5.2.1
Django==3.2.10
Pillow==8.4.0
redis==3.5.3
requests==2.26.0
celery==5.2.3
Django==4.0.1
Pillow==9.0.0
redis==4.1.0
requests==2.27.1
ryd-client==0.0.3
uWSGI==2.0.20
whitenoise==5.3.0
yt_dlp==2021.12.1
yt_dlp==2021.12.27

View File

@@ -200,14 +200,16 @@ button:hover {
}
.toggleBox > .onbtn {
bottom: 15px;
left: 15px;
right: 35%;
top: 45%;
transform: translate(50%,-50%);
font-family: Sen-Regular, sans-serif;
}
.toggleBox > .ofbtn {
bottom: 15px;
right: 15px;
left: 0;
top: 45%;
transform: translate(50%,-50%);
font-family: Sen-Regular, sans-serif;
color: var(--main-font);
}
@@ -273,7 +275,7 @@ button:hover {
.sort {
display: flex;
flex-wrap: wrap;
align-content: center;
align-items: center;
}
.padding-box {
@@ -288,23 +290,9 @@ button:hover {
width: 50%;
}
.search-form input {
width: 90%;
}
.search-icon {
flex: 0 0 40px;
}
.search-icon img {
width: 100%;
cursor: pointer;
filter: var(--img-filter);
}
.view-controls {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-columns: 1fr auto auto;
border-bottom: 2px solid;
border-color: var(--accent-font-dark);
margin: 15px 0;
@@ -322,12 +310,6 @@ button:hover {
filter: var(--img-filter);
}
#search-box {
display: none;
flex: auto;
padding: 10px;
}
#hidden-form {
display: none;
}
@@ -485,6 +467,23 @@ button:hover {
/* info box */
.title-split {
display: grid;
grid-template-columns: 1fr 1fr;
}
.title-split img {
width: 40px;
filter: var(--img-filter);
cursor: pointer;
margin: 0 10px;
}
.title-split-form {
padding-top: 30px;
display: flex;
}
.info-box {
display: grid;
grid-gap: 1rem;
@@ -651,6 +650,15 @@ button:hover {
width: 100%;
}
/* multi search page */
.multi-search-box input {
width: 100%;
}
.multi-search-result {
padding: 1rem 0;
}
/* channel overview page */
.channel-list.list {
display: block;
@@ -729,7 +737,8 @@ button:hover {
align-content: center;
}
.playlist-desc.list > * {
.playlist-desc.list > a,
.playlist-desc.list > p {
width: 100%;
}
@@ -1026,6 +1035,10 @@ button:hover {
.footer {
text-align: center;
}
.footer .boxed-content span {
width: 100%;
display: block;
}
.toggle {
flex-wrap: wrap;
}

View File

@@ -1,68 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="500"
height="500"
viewBox="0 0 132.29197 132.29167"
version="1.1"
id="svg1303"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="Icons_Help.svg">
<defs
id="defs1297" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1.0105705"
inkscape:cx="58.993585"
inkscape:cy="143.32456"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1" />
<metadata
id="metadata1300">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Ebene 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-164.70764)">
<g
transform="matrix(6.2585232,0,0,6.2585232,-555.66096,-762.59652)"
id="g1069"
style="fill:#000000">
<path
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
d="M 99.35362,148.32944 A 10.406119,10.406119 0 0 0 88.947542,158.73553 10.406119,10.406119 0 0 0 99.35362,169.14162 10.406119,10.406119 0 0 0 109.7597,158.73553 10.406119,10.406119 0 0 0 99.35362,148.32944 Z m 0.022,2.79535 c 0.58489,0 1.12588,0.073 1.62304,0.2192 0.51178,0.14622 0.95018,0.37308 1.31574,0.68016 0.38018,0.30706 0.67262,0.70157 0.87734,1.18411 0.21933,0.46791 0.32933,1.02372 0.32933,1.6671 0,0.4533 -0.0809,0.86261 -0.24176,1.22816 -0.16084,0.35094 -0.3655,0.67987 -0.61408,0.98694 -0.23396,0.30706 -0.48991,0.60692 -0.76774,0.89936 -0.26321,0.27783 -0.51176,0.57027 -0.74571,0.87734 -0.21932,0.30707 -0.39436,0.63601 -0.52597,0.98694 -0.1316,0.33631 -0.17549,0.72394 -0.13163,1.16261 h -2.829714 c -0.07311,-0.49715 -0.0583,-0.95035 0.04406,-1.35979 0.116978,-0.40941 0.277739,-0.78965 0.482453,-1.14059 0.204712,-0.35093 0.431586,-0.67246 0.680164,-0.96491 0.263203,-0.29244 0.511747,-0.57062 0.745707,-0.83381 0.23395,-0.27783 0.4238,-0.54119 0.57002,-0.78977 0.16086,-0.26321 0.24123,-0.53344 0.24123,-0.81125 0,-0.4533 -0.13908,-0.79704 -0.41691,-1.03099 -0.2632,-0.23397 -0.61381,-0.35083 -1.052474,-0.35083 -0.409426,0 -0.774845,0.0952 -1.096536,0.28528 -0.307068,0.19009 -0.606919,0.43123 -0.899364,0.72368 l -1.7767,-1.62304 c 0.511782,-0.59952 1.118345,-1.08235 1.820217,-1.4479 0.716494,-0.36557 1.506571,-0.548 2.369287,-0.548 z m -0.307306,11.16414 c 0.555636,0 1.016256,0.19727 1.381816,0.59205 0.38018,0.38019 0.57003,0.86249 0.57003,1.44737 0,0.57028 -0.18985,1.05311 -0.57003,1.4479 -0.36556,0.38018 -0.82618,0.57002 -1.381816,0.57002 -0.555651,0 -1.016262,-0.18984 -1.38182,-0.57002 -0.36556,-0.39479 -0.548536,-0.87762 -0.548536,-1.4479 0,-0.58488 0.182976,-1.06718 0.548536,-1.44737 0.365558,-0.39478 0.826169,-0.59205 1.38182,-0.59205 z"
id="path1038"
inkscape:connector-curvature="0" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="500"
height="500"
viewBox="0 0 132.29197 132.29167"
version="1.1"
id="svg1303"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="Icons_sort.svg">
<defs
id="defs1297" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.6482696"
inkscape:cx="214.8721"
inkscape:cy="136.02434"
inkscape:document-units="mm"
inkscape:current-layer="g855"
showgrid="false"
units="px"
inkscape:window-width="957"
inkscape:window-height="893"
inkscape:window-x="941"
inkscape:window-y="13"
inkscape:window-maximized="0" />
<metadata
id="metadata1300">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Ebene 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-164.70764)">
<g
id="g855"
transform="matrix(1.9016362,0,0,1.9016362,-197.93838,-58.9418)">
<path
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.55118108;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
d="M 341.125 82.275391 L 315.50977 106.92773 L 241.84375 177.89258 L 266.21289 203.17969 L 309.82617 161.17969 L 306.72266 379.375 C 306.36114 398.60059 319.37807 414.32761 335.91016 414.63867 L 335.91406 414.63477 C 352.44586 414.94597 366.04647 399.71987 366.4082 380.49414 L 369.5 162.97852 L 411.04297 206.11719 L 436.64062 181.44727 L 365.48242 107.57812 L 365.49609 107.5625 L 341.125 82.275391 z M 175.93359 82.277344 L 175.92969 82.28125 C 159.39782 81.970041 145.79728 97.196144 145.43555 116.42188 L 142.34375 333.9375 L 100.80078 290.79883 L 75.203125 315.46875 L 146.36133 389.33789 L 146.3457 389.35156 L 170.7168 414.63867 L 196.33203 389.98633 L 270 319.02344 L 245.63086 293.73633 L 202.01758 335.73633 L 205.12109 117.54102 C 205.48261 98.315428 192.46568 82.588409 175.93359 82.277344 z "
transform="matrix(0.13913489,0,0,0.13913489,104.08846,117.60887)"
id="path814" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -46,17 +46,19 @@ function isUnwatched(youtube_id) {
}
function unsubscribe(id_unsub) {
console.log(payload);
var payload = JSON.stringify({'unsubscribe': id_unsub});
sendPost(payload);
document.getElementById(id_unsub).replaceWith("You are unsubscribed.");
var message = document.createElement('span');
message.innerText = "You are unsubscribed.";
document.getElementById(id_unsub).replaceWith(message);
}
function subscribe(id_sub) {
var payload = JSON.stringify({'subscribe': id_sub});
console.log(payload);
sendPost(payload);
document.getElementById(id_sub).replaceWith("You are subscribed.");
var message = document.createElement('span');
message.innerText = "You are subscribed.";
document.getElementById(id_sub).replaceWith(message);
}
function changeView(image) {
@@ -347,7 +349,10 @@ function removePlayer() {
if (playerElement.hasChildNodes()) {
var youtubeId = playerElement.childNodes[0].getAttribute("data-id");
var playedStatus = document.createDocumentFragment();
playedStatus.appendChild(document.getElementById(youtubeId));
var playedBox = document.getElementById(youtubeId);
if (playedBox) {
playedStatus.appendChild(playedBox);
};
playerElement.innerHTML = '';
// append played status
var videoInfo = document.getElementById('video-info-' + youtubeId);
@@ -355,54 +360,182 @@ function removePlayer() {
};
}
// searching channels
function searchChannels(query) {
var searchResultBox = document.getElementById('resultBox');
searchResultBox.innerHTML = '';
// multi search form
function searchMulti(query) {
if (query.length > 1) {
var payload = JSON.stringify({'channel-search': query})
sendSearchAsYouType(payload);
};
}
function populateChannelResults(allResults) {
var searchResultBox = document.getElementById('resultBox');
for (let i = 0; i < allResults.length; i++) {
var singleResult = allResults[i];
var source = singleResult['source'];
var channelName = source['channel_name'];
var channelId = source['channel_id'];
var optionElement = document.createElement('option');
optionElement.value = channelName;
optionElement.setAttribute('data', channelId);
searchResultBox.appendChild(optionElement);
};
}
function channelRedirect(){
var response = document.getElementById('resultBox');
var firstChild = response.firstChild
if (firstChild) {
var redirectId = firstChild.getAttribute('data');
location = '/channel/' + redirectId;
};
return false;
}
function sendSearchAsYouType(payload) {
var http = new XMLHttpRequest();
http.onreadystatechange = function() {
if (http.readyState === 4) {
allResults = JSON.parse(http.response)['results'];
populateChannelResults(allResults);
var payload = JSON.stringify({'multi_search': query})
var http = new XMLHttpRequest();
http.onreadystatechange = function() {
if (http.readyState === 4) {
allResults = JSON.parse(http.response)['results'];
populateMultiSearchResults(allResults);
};
};
http.open("POST", "/process/", true);
http.setRequestHeader("X-CSRFToken", getCookie("csrftoken"));
http.setRequestHeader("Content-type", "application/json");
http.send(payload);
};
http.open("POST", "/process/", true);
http.setRequestHeader("X-CSRFToken", getCookie("csrftoken"));
http.setRequestHeader("Content-type", "application/json");
http.send(payload);
}
function getViewDefaults(view) {
var defaultView = document.getElementById("id_" + view).value;
return defaultView
}
function populateMultiSearchResults(allResults) {
// videos
var defaultVideo = getViewDefaults("home");
var allVideos = allResults["video_results"];
var videoBox = document.getElementById("video-results");
videoBox.innerHTML = "";
for (let index = 0; index < allVideos.length; index++) {
const video = allVideos[index]["source"];
const videoDiv = createVideo(video, defaultVideo);
videoBox.appendChild(videoDiv);
};
// channels
var defaultChannel = getViewDefaults("channel");
var allChannels = allResults["channel_results"];
var channelBox = document.getElementById("channel-results");
channelBox.innerHTML = "";
for (let index = 0; index < allChannels.length; index++) {
const channel = allChannels[index]["source"];
const channelDiv = createChannel(channel, defaultChannel);
channelBox.appendChild(channelDiv);
};
// playlists
var defaultPlaylist = getViewDefaults("playlist");
var allPlaylists = allResults["playlist_results"];
var playlistBox = document.getElementById("playlist-results");
playlistBox.innerHTML = "";
for (let index = 0; index < allPlaylists.length; index++) {
const playlist = allPlaylists[index]["source"];
const playlistDiv = createPlaylist(playlist, defaultPlaylist);
playlistBox.appendChild(playlistDiv);
};
}
function createVideo(video, viewStyle) {
// create video item div from template
const videoId = video["youtube_id"];
const mediaUrl = video["media_url"];
const thumbUrl = "/cache/" + video["vid_thumb_url"];
const videoTitle = video["title"];
const videoPublished = video["published"];
const videoDuration = video["player"]["duration_str"];
if (video["player"]["watched"]) {
var playerState = "seen";
} else {
var playerState = "unseen";
};
const channelId = video["channel"]["channel_id"];
const channelName = video["channel"]["channel_name"];
// build markup
const markup = `
<a href="#player" data-src="/media/${mediaUrl}" data-thumb="${thumbUrl}" data-title="${videoTitle}" data-channel="${channelName}" data-channel-id="${channelId}" data-id="${videoId}" onclick="createPlayer(this)">
<div class="video-thumb-wrap ${viewStyle}">
<div class="video-thumb">
<img src="${thumbUrl}" alt="video-thumb">
</div>
<div class="video-play">
<img src="/static/img/icon-play.svg" alt="play-icon">
</div>
</div>
</a>
<div class="video-desc ${viewStyle}">
<div class="video-desc-player" id="video-info-${videoId}">
<img src="/static/img/icon-${playerState}.svg" alt="${playerState}-icon" id="${videoId}" onclick="isWatched(this.id)" class="${playerState}-icon">
<span>${videoPublished} | ${videoDuration}</span>
</div>
<div>
<a href="/channel/${channelId}/"><h3>${channelName}</h3></a>
<a class="video-more" href="/video/${videoId}/"><h2>${videoTitle}</h2></a>
</div>
</div>
`
const videoDiv = document.createElement("div");
videoDiv.setAttribute("class", "video-item " + viewStyle);
videoDiv.innerHTML = markup
return videoDiv
}
function createChannel(channel, viewStyle) {
// create channel item div from template
const channelId = channel["channel_id"];
const channelName = channel["channel_name"];
const channelSubs = channel["channel_subs"];
const channelLastRefresh = channel["channel_last_refresh"];
if (channel["channel_subscribed"]) {
var button = `<button class="unsubscribe" type="button" id="${channelId}" onclick="unsubscribe(this.id)" title="Unsubscribe from ${channelName}">Unsubscribe</button>`
} else {
var button = `<button type="button" id="${channelId}" onclick="subscribe(this.id)" title="Subscribe to ${channelName}">Subscribe</button>`
};
// build markup
const markup = `
<div class="channel-banner ${viewStyle}">
<a href="/channel/${channelId}/">
<img src="/cache/channels/${channelId}_banner.jpg" alt="${channelId}-banner">
</a>
</div>
<div class="info-box info-box-2 ${viewStyle}">
<div class="info-box-item">
<div class="round-img">
<a href="/channel/${channelId}/">
<img src="/cache/channels/${channelId}_thumb.jpg" alt="channel-thumb">
</a>
</div>
<div>
<h3><a href="/channel/${channelId}/">${channelName}</a></h3>
<p>Subscribers: ${channelSubs}</p>
</div>
</div>
<div class="info-box-item">
<div>
<p>Last refreshed: ${channelLastRefresh}</p>
${button}
</div>
</div>
</div>
`
const channelDiv = document.createElement("div");
channelDiv.setAttribute("class", "channel-item " + viewStyle);
channelDiv.innerHTML = markup;
return channelDiv
}
function createPlaylist(playlist, viewStyle) {
// create playlist item div from template
const playlistId = playlist["playlist_id"];
const playlistName = playlist["playlist_name"];
const playlistChannelId = playlist["playlist_channel_id"];
const playlistChannel = playlist["playlist_channel"];
const playlistLastRefresh = playlist["playlist_last_refresh"];
if (playlist["playlist_subscribed"]) {
var button = `<button class="unsubscribe" type="button" id="${playlistId}" onclick="unsubscribe(this.id)" title="Unsubscribe from ${playlistName}">Unsubscribe</button>`
} else {
var button = `<button type="button" id="${playlistId}" onclick="subscribe(this.id)" title="Subscribe to ${playlistName}">Subscribe</button>`
};
const markup = `
<div class="playlist-thumbnail">
<a href="/playlist/${playlistId}/">
<img src="/cache/playlists/${playlistId}.jpg" alt="${playlistId}-thumbnail">
</a>
</div>
<div class="playlist-desc ${viewStyle}">
<a href="/channel/${playlistChannelId}/"><h3>${playlistChannel}</h3></a>
<a href="/playlist/${playlistId}/"><h2>${playlistName}</h2></a>
<p>Last refreshed: ${playlistLastRefresh}</p>
${button}
</div>
`
const playlistDiv = document.createElement("div");
playlistDiv.setAttribute("class", "playlist-item " + viewStyle);
playlistDiv.innerHTML = markup;
return playlistDiv
}
@@ -444,32 +577,22 @@ function textReveal() {
};
}
function showSearch() {
var searchBox = document.getElementById('search-box');
var displayStyle = searchBox.style.display
if (displayStyle === "") {
searchBox.style.display = 'block';
} else {
searchBox.style.display = "";
}
var inputBox = document.getElementById('id_searchInput');
inputBox.focus();
}
function showForm() {
var formElement = document.getElementById('hidden-form');
var displayStyle = formElement.style.display
if (displayStyle === "") {
formElement.style.display = 'block';
animate('add-icon', 'pulse-img');
} else {
formElement.style.display = "";
}
};
animate('animate-icon', 'pulse-img');
}
function animate(elementId, animationClass) {
var toAnimate = document.getElementById(elementId);
if (toAnimate.className !== animationClass) {
toAnimate.className = animationClass;
}
} else {
toAnimate.classList.remove(animationClass);
};
}