Compare commits

...

81 Commits

Author SHA1 Message Date
Simon
fe8f4faa10 update TA_VERSION 2023-12-21 10:32:53 +07:00
Simon
ddc0b7a481 Various yt-dlp fixes, #build
Changed:
- Fix for channel about page parsing
- Fix for like_count extraction
- Fix for comment author extraction
- Refactor RedisQueue use sorted set
- Fix chrome scaling issue
- Fix autodelete
2023-12-17 11:34:46 +07:00
Simon
7eec3ece49 lock yt-dlp at commit 2023-12-17 11:28:36 +07:00
Simon
789c35e2b5 refactor RedisQueue to use sorted set 2023-12-17 11:22:26 +07:00
Simon
8870782a6e refactor, use decode_responses in base class 2023-12-16 17:39:09 +07:00
Simon
e75ffb603c fix auto delete lte datatype, #622 2023-12-16 13:13:48 +07:00
Simon
feabc87c9f fix chrome scaling issue, #616 2023-12-16 12:44:19 +07:00
Simon
6f1a45ffb1 downgrade redis 2023-12-16 10:18:40 +07:00
Simon
098db97cba revert channel about parsing, #614 2023-12-12 14:00:56 +07:00
Simon
597da56975 fix comment_author_is_uploader extraction 2023-12-12 13:55:41 +07:00
Simon
325bdf5cba add unstable tag, #build 2023-12-03 15:19:18 +07:00
Simon
db2f249979 Channel about parser workaround, #build
Changed:
- Use /featured endpoint for channel parsing
- Improved version check cleanup process
- Handle version check random channel building errors
2023-12-03 14:50:50 +07:00
Simon
b61b8635b8 bump celery 2023-12-03 14:49:59 +07:00
Simon
5aafc21bda use featured path to extract channel metadata, #614 2023-12-03 14:48:56 +07:00
lamusmaser
099c70a13b Add check to determine if sub_value is rand-d, always. (#612) 2023-12-01 09:23:58 +07:00
Simon
43708ee2a3 refac _has_update parser, use tpl comparison 2023-11-22 12:46:09 +07:00
Simon
cfb15c1a78 handle version check comparison over any diff 2023-11-22 10:49:04 +07:00
Simon
e9a95d7ada bump TA_VERSION 2023-11-21 12:50:53 +07:00
Simon
a21a111221 rename to Videos 2023-11-20 14:56:22 +07:00
Simon
18e504faf2 fix add missining playlist_entries mappings, #605 2023-11-20 14:02:49 +07:00
Simon
9ffe2098a5 add unstable version 2023-11-19 22:10:07 +07:00
Simon
1315e836a4 Improved dashboard, reindex fix, #build
Changed:
- Added additional sort by fields
- [API] Chaned primary stats endpoints
- [API] Added separate video stats endpoints
- Added fallback for some manual import values
- Fix comment extration for members video
- Fix reindex outdated query
2023-11-19 22:02:23 +07:00
Simon
2e4289e75c bump requirements 2023-11-19 21:37:08 +07:00
Simon
96e73a3a53 handle empty tile response 2023-11-19 21:32:11 +07:00
Simon
a369be0f4a split active videos tile, add duration 2023-11-19 21:20:42 +07:00
Simon
d5676e5173 [API] remove primary endpoint, in favor of dedicated stats 2023-11-19 20:30:50 +07:00
Simon
44c4cf93e2 refactor dashboard tile building 2023-11-19 20:27:18 +07:00
Simon
02ac590caa [API] add download stats 2023-11-19 14:42:16 +07:00
Simon
a466c02304 [API] add playlist stats 2023-11-19 14:00:27 +07:00
Simon
e74c26fe36 [API] add channel aggs 2023-11-19 13:48:24 +07:00
Simon
b1267cba83 standard json style 2023-11-19 13:06:47 +07:00
Simon
91bb0ed9c0 [API] add video aggregation 2023-11-19 13:01:27 +07:00
Simon
4a145ee7cb paginate to get total active docs count 2023-11-18 17:44:16 +07:00
Simon
463019ce5a fix outdated redinex now_lte datatype 2023-11-18 17:30:31 +07:00
Simon
9a9d35cac4 explicitly define player mapping, #592 2023-11-17 09:44:10 +07:00
Simon
f41ecd24c5 fix missing config for comments extraction, #596 2023-11-17 09:26:31 +07:00
crocs
eced8200c1 Update settings_scheduling.html (#601)
I found more!
2023-11-17 09:23:19 +07:00
Simon
669bc6a620 fallback for view_count, refac, #581 2023-11-17 09:22:11 +07:00
lamusmaser
37df9b65c7 Add allowed_null_keys and its dictionary for manual imports. (#595)
* Add `allowed_null_keys` and its dictionary for manual imports.

* Fix linting for `allowed_null_keys` list.

* Add missing trailing comma for linting.

* Add missing newline that wasn't in earlier linting responses.

* Clear empty text in newlines.

* Remove newline that the linter requested because the linter now doesn't want it. ¯\_(ツ)_/¯

* Change default application from manual import to the video processing.

* Fix missing space.
2023-11-17 09:16:09 +07:00
lamusmaser
6721d01fa6 Fix textarea type from shell to Shell. (#594) 2023-11-17 09:12:02 +07:00
crocs
2b49af9620 Update settings.html (#599)
This was really bugging me lol
2023-11-15 11:45:08 +07:00
Derek Slenk
2f62898a10 Add new css item for web footer (#598) 2023-11-15 11:44:43 +07:00
spechter
832259ce48 Expanded sorting functionality (#589)
* - Added duration and filesize as options in sorting menu on Home and ChannelId views
- Added keys 'duration' and 'filesize' as valid parameters to sort by
- Mapped 'duration' and 'filesize' to their corresponding es keys

* Fixed spelling

* Changed formatting to comply to maximum line length.

* Locally running "deploy.sh validate" before committing

---------

Co-authored-by: spechter <spechter@spechter.net>
2023-11-15 11:06:51 +07:00
Simon
b8ccce250a bump TA_VERSION 2023-11-10 10:34:04 +07:00
Simon
aa04ecff4f bump es 2023-11-10 10:33:53 +07:00
Simon
dcf97d3d24 tweak color matrix color filter 2023-11-10 09:57:18 +07:00
crocs
879ad52b32 updated icons (#588)
* icon updates

* Update icon-star-half.svg
2023-11-10 09:40:17 +07:00
Simon
0bedc3ee93 fix empty watchDetail building 2023-11-09 11:55:28 +07:00
Simon
1657c55cbe Aggregation daily stats improvements, #build
Changed:
- [API] make daily stats TZ aware
- [API] add daily download media size
2023-11-09 10:42:02 +07:00
Simon
8b1324139d pass time_zone to daily aggs 2023-11-09 10:34:08 +07:00
Simon
04124e3dad add daily size download 2023-11-09 10:22:43 +07:00
Simon
9c26357f76 User conf endpoints, fix channel parser, #build
Changed:
- [API] Added endpoints to CRUD user conf vars
- [API] Added backup endpoints
- Fix channel about page parsing
- Add custom CSS files
- Remember player volume
2023-11-09 09:40:54 +07:00
extome
7133d6b441 Better CSS support (#583)
* Remove banner hardcoding

* Refactor "colors" to "stylesheet"

* Remove logo hardcoding

* Remove stylesheet hardcoding

* Add very basic static CSS scanning and a new style

* Respect environment settings

* Check if selected stylesheet still exists

* New theme and title formatting

* Revert migration change

* Code linting

* More outlines for Matrix style

* Change wording in settings

* Forgot this wording

* Add suggested changes
2023-11-09 09:33:03 +07:00
Simon
6bc0111d0a set and get playerVolume from localStorage 2023-11-09 09:31:19 +07:00
Simon
1188e66f37 fix channel about page parsing, #587 2023-11-08 23:20:13 +07:00
Simon
ef6d3e868d bump requirements 2023-11-08 23:09:55 +07:00
Simon
d677f9579e replace old process view, use user conf api 2023-11-01 22:49:33 +07:00
Simon
0b920e87ae [API] add user config endpoints 2023-11-01 19:07:22 +07:00
Simon
4d5aa4ad2f validate user config values 2023-11-01 17:25:22 +07:00
Simon
4b63c2f536 simplify return message 2023-11-01 14:33:30 +07:00
Simon
31ad9424f5 remove unused db_restore 2023-11-01 14:10:45 +07:00
Simon
45f4ccfd93 fix off by one in filesystem rescan progress 2023-11-01 14:07:56 +07:00
Simon
285e2042ae [API] add backup endpoints 2023-11-01 14:05:11 +07:00
Simon
e4b7f8ce38 update roadmap 2023-11-01 11:04:21 +07:00
Simon
6892cbbc19 Read only user roles, refac env var builder, #build
Changed:
- Added view only user role
- Fixed media download URL builder
- Changed environment settings builder away from redis
- Improved dashboard
2023-11-01 09:24:21 +07:00
Simon
58ea256b44 add unstable tag 2023-11-01 09:19:18 +07:00
Merlin
aa475c58aa Refac settings dashboard (#577)
* Add padding to duration str text

* Add singular and plural to video in dailyStat

* Add code spacing for readability

* Refac Main overview in dashboard to be spaced evenly and use tables

* Refac simplify number padding

* Refac skip adding spacing rows on mobile

* Refac reorder watch progress to be in order of interest

* Fix that ther can be 0 Videos added a day

* Refac capitalize content keys
2023-11-01 08:40:41 +07:00
Simon
8247314d01 refactor admin permisson classes 2023-10-31 15:50:33 +07:00
Simon
2826ca4a43 move ES_SNAPSHOT_DIR to EnvironmentSettings 2023-10-28 15:25:57 +07:00
Simon
64ffc18da7 add debug methods for EnvironmentSettings 2023-10-28 15:16:22 +07:00
Simon
21fde5e068 remove old migrations 2023-10-28 15:03:16 +07:00
Simon
ea9ed6c238 fix linter 2023-10-28 10:30:21 +07:00
Simon
8eaed07cff remove unused renamer 2023-10-28 10:29:10 +07:00
Clark
4d111aff82 Move the startup application settings to a new class (#571)
* Move the startup application settings to a new class

* Replace settings methods with static fields

* Move Redis and ES configuration to the settings class

* Fix environment python imports

* Update envcheck to use the new settings
2023-10-28 10:27:03 +07:00
Simon
7236bea29a add error setting rlimit to common errors 2023-10-20 15:36:59 +07:00
Simon
5165c3e34a bump requirements 2023-10-16 16:12:28 +07:00
Simon
572b23169c finetune limited permission user 2023-10-15 14:56:54 +07:00
Steve Ovens
e1fce06f97 View only user (#539)
* Remove repo docs in favor of hosted docs (#537)

* updated base, channel, video htmls to hide elements based on if user is staff or in the group 'admin'

* added the load auth_extras

* updated auth_extras

* updated views.py to block api calls from deleting files from unprivileged users; The Templates needed to be updated to support the various group checks related to removing buttons an unprivileged user should not see

* bumped the channel templates to remove conflict

* fix linting issues

* more linting

---------

Co-authored-by: Merlin <4706504+MerlinScheurer@users.noreply.github.com>
2023-10-15 13:58:06 +07:00
Simon
446d5b7949 Merge branch 'master' into testing 2023-10-15 12:03:08 +07:00
Simon
17c0310220 bump docker compose version, #569 2023-10-13 08:32:40 +07:00
Omar Laham
1b0be84972 Remove /media/ prefix from Download File URL in video.html (#567) 2023-10-13 08:20:42 +07:00
76 changed files with 1707 additions and 1623 deletions

View File

@@ -38,6 +38,6 @@ body:
attributes:
label: Relevant log output
description: Please copy and paste any relevant Docker logs. This will be automatically formatted into code, so no need for backticks.
render: shell
render: Shell
validations:
required: true

View File

@@ -135,6 +135,11 @@ The Elasticsearch index will turn to ***read only*** if the disk usage of the co
Similar to that, TubeArchivist will become all sorts of messed up when running out of disk space. There are some error messages in the logs when that happens, but it's best to make sure to have enough disk space before starting to download.
## `error setting rlimit`
If you are seeing errors like `failed to create shim: OCI runtime create failed` and `error during container init: error setting rlimits`, this means docker can't set these limits, usually because they are set at another place or are incompatible because of other reasons. Solution is to remove the `ulimits` key from the ES container in your docker compose and start again.
This can happen if you have nested virtualizations, e.g. LXC running Docker in Proxmox.
## Known limitations
- Video files created by Tube Archivist need to be playable in your browser of choice. Not every codec is compatible with every browser and might require some testing with format selection.
- Every limitation of **yt-dlp** will also be present in Tube Archivist. If **yt-dlp** can't download or extract a video for any reason, Tube Archivist won't be able to either.
@@ -144,6 +149,7 @@ Similar to that, TubeArchivist will become all sorts of messed up when running o
We have come far, nonetheless we are not short of ideas on how to improve and extend this project. Issues waiting for you to be tackled in no particular order:
- [ ] User roles
- [ ] Audio download
- [ ] Podcast mode to serve channel as mp3
- [ ] User created playlists, random and repeat controls ([#108](https://github.com/tubearchivist/tubearchivist/issues/108), [#220](https://github.com/tubearchivist/tubearchivist/issues/220))
- [ ] Auto play or play next link ([#226](https://github.com/tubearchivist/tubearchivist/issues/226))
@@ -153,6 +159,7 @@ We have come far, nonetheless we are not short of ideas on how to improve and ex
- [ ] Custom searchable notes to videos, channels, playlists ([#144](https://github.com/tubearchivist/tubearchivist/issues/144))
- [ ] Search comments
- [ ] Search download queue
- [ ] Configure shorts, streams and video sizes per channel
Implemented:
- [X] Add statistics of index [2023-09-03]

View File

@@ -1,4 +1,4 @@
version: '3.3'
version: '3.5'
services:
tubearchivist:
@@ -40,7 +40,7 @@ services:
depends_on:
- archivist-es
archivist-es:
image: bbilly1/tubearchivist-es # only for amd64, or use official es 8.9.0
image: bbilly1/tubearchivist-es # only for amd64, or use official es 8.11.0
container_name: archivist-es
restart: unless-stopped
environment:

View File

@@ -2,6 +2,7 @@
from home.src.es.connect import ElasticWrap
from home.src.ta.helper import get_duration_str
from home.src.ta.settings import EnvironmentSettings
class AggBase:
@@ -23,62 +24,159 @@ class AggBase:
raise NotImplementedError
class Primary(AggBase):
"""primary aggregation for total documents indexed"""
class Video(AggBase):
"""get video stats"""
name = "primary"
path = "ta_video,ta_channel,ta_playlist,ta_subtitle,ta_download/_search"
name = "video_stats"
path = "ta_video/_search"
data = {
"size": 0,
"aggs": {
"video_type": {
"filter": {"exists": {"field": "active"}},
"aggs": {"filtered": {"terms": {"field": "vid_type"}}},
"terms": {"field": "vid_type"},
"aggs": {
"media_size": {"sum": {"field": "media_size"}},
"duration": {"sum": {"field": "player.duration"}},
},
},
"channel_total": {"value_count": {"field": "channel_active"}},
"channel_sub": {"terms": {"field": "channel_subscribed"}},
"playlist_total": {"value_count": {"field": "playlist_active"}},
"playlist_sub": {"terms": {"field": "playlist_subscribed"}},
"download": {"terms": {"field": "status"}},
"video_active": {
"terms": {"field": "active"},
"aggs": {
"media_size": {"sum": {"field": "media_size"}},
"duration": {"sum": {"field": "player.duration"}},
},
},
"video_media_size": {"sum": {"field": "media_size"}},
"video_count": {"value_count": {"field": "youtube_id"}},
"duration": {"sum": {"field": "player.duration"}},
},
}
def process(self):
"""make the call"""
"""process aggregation"""
aggregations = self.get()
videos = {"total": aggregations["video_type"].get("doc_count")}
videos.update(
{
i.get("key"): i.get("doc_count")
for i in aggregations["video_type"]["filtered"]["buckets"]
}
)
channels = {"total": aggregations["channel_total"].get("value")}
channels.update(
{
"sub_" + i.get("key_as_string"): i.get("doc_count")
for i in aggregations["channel_sub"]["buckets"]
}
)
playlists = {"total": aggregations["playlist_total"].get("value")}
playlists.update(
{
"sub_" + i.get("key_as_string"): i.get("doc_count")
for i in aggregations["playlist_sub"]["buckets"]
}
)
downloads = {
i.get("key"): i.get("doc_count")
for i in aggregations["download"]["buckets"]
duration = int(aggregations["duration"]["value"])
response = {
"doc_count": aggregations["video_count"]["value"],
"media_size": int(aggregations["video_media_size"]["value"]),
"duration": duration,
"duration_str": get_duration_str(duration),
}
for bucket in aggregations["video_type"]["buckets"]:
duration = int(bucket["duration"].get("value"))
response.update(
{
f"type_{bucket['key']}": {
"doc_count": bucket.get("doc_count"),
"media_size": int(bucket["media_size"].get("value")),
"duration": duration,
"duration_str": get_duration_str(duration),
}
}
)
for bucket in aggregations["video_active"]["buckets"]:
duration = int(bucket["duration"].get("value"))
response.update(
{
f"active_{bucket['key_as_string']}": {
"doc_count": bucket.get("doc_count"),
"media_size": int(bucket["media_size"].get("value")),
"duration": duration,
"duration_str": get_duration_str(duration),
}
}
)
return response
class Channel(AggBase):
"""get channel stats"""
name = "channel_stats"
path = "ta_channel/_search"
data = {
"size": 0,
"aggs": {
"channel_count": {"value_count": {"field": "channel_id"}},
"channel_active": {"terms": {"field": "channel_active"}},
"channel_subscribed": {"terms": {"field": "channel_subscribed"}},
},
}
def process(self):
"""process aggregation"""
aggregations = self.get()
response = {
"videos": videos,
"channels": channels,
"playlists": playlists,
"downloads": downloads,
"doc_count": aggregations["channel_count"].get("value"),
}
for bucket in aggregations["channel_active"]["buckets"]:
key = f"active_{bucket['key_as_string']}"
response.update({key: bucket.get("doc_count")})
for bucket in aggregations["channel_subscribed"]["buckets"]:
key = f"subscribed_{bucket['key_as_string']}"
response.update({key: bucket.get("doc_count")})
return response
class Playlist(AggBase):
"""get playlist stats"""
name = "playlist_stats"
path = "ta_playlist/_search"
data = {
"size": 0,
"aggs": {
"playlist_count": {"value_count": {"field": "playlist_id"}},
"playlist_active": {"terms": {"field": "playlist_active"}},
"playlist_subscribed": {"terms": {"field": "playlist_subscribed"}},
},
}
def process(self):
"""process aggregation"""
aggregations = self.get()
response = {"doc_count": aggregations["playlist_count"].get("value")}
for bucket in aggregations["playlist_active"]["buckets"]:
key = f"active_{bucket['key_as_string']}"
response.update({key: bucket.get("doc_count")})
for bucket in aggregations["playlist_subscribed"]["buckets"]:
key = f"subscribed_{bucket['key_as_string']}"
response.update({key: bucket.get("doc_count")})
return response
class Download(AggBase):
"""get downloads queue stats"""
name = "download_queue_stats"
path = "ta_download/_search"
data = {
"size": 0,
"aggs": {
"status": {"terms": {"field": "status"}},
"video_type": {
"filter": {"term": {"status": "pending"}},
"aggs": {"type_pending": {"terms": {"field": "vid_type"}}},
},
},
}
def process(self):
"""process aggregation"""
aggregations = self.get()
response = {}
for bucket in aggregations["status"]["buckets"]:
response.update({bucket["key"]: bucket.get("doc_count")})
for bucket in aggregations["video_type"]["type_pending"]["buckets"]:
key = f"pending_{bucket['key']}"
response.update({key: bucket.get("doc_count")})
return response
@@ -117,7 +215,7 @@ class WatchProgress(AggBase):
all_duration = int(aggregations["total_duration"].get("value"))
response.update(
{
"all": {
"total": {
"duration": all_duration,
"duration_str": get_duration_str(all_duration),
"items": aggregations["total_vids"].get("value"),
@@ -168,13 +266,22 @@ class DownloadHist(AggBase):
"calendar_interval": "day",
"format": "yyyy-MM-dd",
"order": {"_key": "desc"},
"time_zone": EnvironmentSettings.TZ,
},
"aggs": {
"total_videos": {"value_count": {"field": "youtube_id"}}
"total_videos": {"value_count": {"field": "youtube_id"}},
"media_size": {"sum": {"field": "media_size"}},
},
}
},
"query": {"range": {"date_downloaded": {"gte": "now-7d/d"}}},
"query": {
"range": {
"date_downloaded": {
"gte": "now-7d/d",
"time_zone": EnvironmentSettings.TZ,
}
}
},
}
def process(self):
@@ -186,6 +293,7 @@ class DownloadHist(AggBase):
{
"date": i.get("key_as_string"),
"count": i.get("doc_count"),
"media_size": i["media_size"].get("value"),
}
for i in buckets
]

View File

@@ -7,15 +7,14 @@ Functionality:
import urllib.parse
from home.src.download.thumbnails import ThumbManager
from home.src.ta.config import AppConfig
from home.src.ta.helper import date_praser, get_duration_str
from home.src.ta.settings import EnvironmentSettings
class SearchProcess:
"""process search results"""
CONFIG = AppConfig().config
CACHE_DIR = CONFIG["application"]["cache_dir"]
CACHE_DIR = EnvironmentSettings.CACHE_DIR
def __init__(self, response):
self.response = response

View File

@@ -96,6 +96,16 @@ urlpatterns = [
views.SnapshotApiView.as_view(),
name="api-snapshot",
),
path(
"backup/",
views.BackupApiListView.as_view(),
name="api-backup-list",
),
path(
"backup/<str:filename>/",
views.BackupApiView.as_view(),
name="api-backup",
),
path(
"task-name/",
views.TaskListView.as_view(),
@@ -111,6 +121,11 @@ urlpatterns = [
views.TaskIDView.as_view(),
name="api-task-id",
),
path(
"config/user/",
views.UserConfigView.as_view(),
name="api-config-user",
),
path(
"cookie/",
views.CookieView.as_view(),
@@ -137,9 +152,24 @@ urlpatterns = [
name="api-notification",
),
path(
"stats/primary/",
views.StatPrimaryView.as_view(),
name="api-stats-primary",
"stats/video/",
views.StatVideoView.as_view(),
name="api-stats-video",
),
path(
"stats/channel/",
views.StatChannelView.as_view(),
name="api-stats-channel",
),
path(
"stats/playlist/",
views.StatPlaylistView.as_view(),
name="api-stats-playlist",
),
path(
"stats/download/",
views.StatDownloadView.as_view(),
name="api-stats-download",
),
path(
"stats/watch/",

View File

@@ -1,6 +1,14 @@
"""all API views"""
from api.src.aggs import BiggestChannel, DownloadHist, Primary, WatchProgress
from api.src.aggs import (
BiggestChannel,
Channel,
Download,
DownloadHist,
Playlist,
Video,
WatchProgress,
)
from api.src.search_processor import SearchProcess
from home.src.download.queue import PendingInteract
from home.src.download.subscriptions import (
@@ -8,6 +16,7 @@ from home.src.download.subscriptions import (
PlaylistSubscription,
)
from home.src.download.yt_dlp_base import CookieHandler
from home.src.es.backup import ElasticBackup
from home.src.es.connect import ElasticWrap
from home.src.es.snapshot import ElasticSnapshot
from home.src.frontend.searching import SearchForm
@@ -18,38 +27,69 @@ from home.src.index.playlist import YoutubePlaylist
from home.src.index.reindex import ReindexProgress
from home.src.index.video import SponsorBlock, YoutubeVideo
from home.src.ta.config import AppConfig, ReleaseVersion
from home.src.ta.settings import EnvironmentSettings
from home.src.ta.ta_redis import RedisArchivist
from home.src.ta.task_manager import TaskCommand, TaskManager
from home.src.ta.urlparser import Parser
from home.src.ta.users import UserConfig
from home.tasks import (
BaseTask,
check_reindex,
download_pending,
extrac_dl,
run_restore_backup,
subscribe_to,
)
from rest_framework import permissions
from rest_framework.authentication import (
SessionAuthentication,
TokenAuthentication,
)
from rest_framework.authtoken.models import Token
from rest_framework.authtoken.views import ObtainAuthToken
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
def check_admin(user):
"""check for admin permission for restricted views"""
return user.is_staff or user.groups.filter(name="admin").exists()
class AdminOnly(permissions.BasePermission):
"""allow only admin"""
def has_permission(self, request, view):
return check_admin(request.user)
class AdminWriteOnly(permissions.BasePermission):
"""allow only admin writes"""
def has_permission(self, request, view):
if request.method in permissions.SAFE_METHODS:
return permissions.IsAuthenticated().has_permission(request, view)
return check_admin(request.user)
class ApiBaseView(APIView):
"""base view to inherit from"""
authentication_classes = [SessionAuthentication, TokenAuthentication]
permission_classes = [IsAuthenticated]
permission_classes = [permissions.IsAuthenticated]
search_base = ""
data = ""
def __init__(self):
super().__init__()
self.response = {"data": False, "config": AppConfig().config}
self.response = {
"data": False,
"config": {
"enable_cast": EnvironmentSettings.ENABLE_CAST,
"downloads": AppConfig().config["downloads"],
},
}
self.data = {"query": {"match_all": {}}}
self.status_code = False
self.context = False
@@ -102,6 +142,7 @@ class VideoApiView(ApiBaseView):
"""
search_base = "ta_video/_doc/"
permission_classes = [AdminWriteOnly]
def get(self, request, video_id):
# pylint: disable=unused-argument
@@ -165,7 +206,6 @@ class VideoProgressView(ApiBaseView):
message = {"position": position, "youtube_id": video_id}
RedisArchivist().set_message(key, message)
self.response = request.data
return Response(self.response)
def delete(self, request, video_id):
@@ -276,6 +316,7 @@ class ChannelApiView(ApiBaseView):
"""
search_base = "ta_channel/_doc/"
permission_classes = [AdminWriteOnly]
def get(self, request, channel_id):
# pylint: disable=unused-argument
@@ -306,6 +347,7 @@ class ChannelApiListView(ApiBaseView):
search_base = "ta_channel/_search/"
valid_filter = ["subscribed"]
permission_classes = [AdminWriteOnly]
def get(self, request):
"""get request"""
@@ -419,6 +461,7 @@ class PlaylistApiListView(ApiBaseView):
"""
search_base = "ta_playlist/_search/"
permission_classes = [AdminWriteOnly]
def get(self, request):
"""handle get request"""
@@ -467,6 +510,7 @@ class PlaylistApiView(ApiBaseView):
"""
search_base = "ta_playlist/_doc/"
permission_classes = [AdminWriteOnly]
def get(self, request, playlist_id):
# pylint: disable=unused-argument
@@ -513,6 +557,7 @@ class DownloadApiView(ApiBaseView):
search_base = "ta_download/_doc/"
valid_status = ["pending", "ignore", "priority"]
permission_classes = [AdminOnly]
def get(self, request, video_id):
# pylint: disable=unused-argument
@@ -559,6 +604,7 @@ class DownloadApiListView(ApiBaseView):
search_base = "ta_download/_search/"
valid_filter = ["pending", "ignore"]
permission_classes = [AdminOnly]
def get(self, request):
"""get request"""
@@ -667,6 +713,8 @@ class SnapshotApiListView(ApiBaseView):
POST: take snapshot now
"""
permission_classes = [AdminOnly]
@staticmethod
def get(request):
"""handle get request"""
@@ -691,6 +739,8 @@ class SnapshotApiView(ApiBaseView):
DELETE: delete a snapshot
"""
permission_classes = [AdminOnly]
@staticmethod
def get(request, snapshot_id):
"""handle get request"""
@@ -725,11 +775,87 @@ class SnapshotApiView(ApiBaseView):
return Response(response)
class BackupApiListView(ApiBaseView):
"""resolves to /api/backup/
GET: returns list of available zip backups
POST: take zip backup now
"""
permission_classes = [AdminOnly]
task_name = "run_backup"
@staticmethod
def get(request):
"""handle get request"""
# pylint: disable=unused-argument
backup_files = ElasticBackup().get_all_backup_files()
return Response(backup_files)
def post(self, request):
"""handle post request"""
# pylint: disable=unused-argument
response = TaskCommand().start(self.task_name)
message = {
"message": "backup task started",
"task_id": response["task_id"],
}
return Response(message)
class BackupApiView(ApiBaseView):
"""resolves to /api/backup/<filename>/
GET: return a single backup
POST: restore backup
DELETE: delete backup
"""
permission_classes = [AdminOnly]
task_name = "restore_backup"
@staticmethod
def get(request, filename):
"""get single backup"""
# pylint: disable=unused-argument
backup_file = ElasticBackup().build_backup_file_data(filename)
if not backup_file:
message = {"message": "file not found"}
return Response(message, status=404)
return Response(backup_file)
def post(self, request, filename):
"""restore backup file"""
# pylint: disable=unused-argument
task = run_restore_backup.delay(filename)
message = {
"message": "backup restore task started",
"filename": filename,
"task_id": task.id,
}
return Response(message)
@staticmethod
def delete(request, filename):
"""delete backup file"""
# pylint: disable=unused-argument
backup_file = ElasticBackup().delete_file(filename)
if not backup_file:
message = {"message": "file not found"}
return Response(message, status=404)
message = {"message": f"file {filename} deleted"}
return Response(message)
class TaskListView(ApiBaseView):
"""resolves to /api/task-name/
GET: return a list of all stored task results
"""
permission_classes = [AdminOnly]
def get(self, request):
"""handle get request"""
# pylint: disable=unused-argument
@@ -744,6 +870,8 @@ class TaskNameListView(ApiBaseView):
POST: start new background process
"""
permission_classes = [AdminOnly]
def get(self, request, task_name):
"""handle get request"""
# pylint: disable=unused-argument
@@ -782,6 +910,7 @@ class TaskIDView(ApiBaseView):
"""
valid_commands = ["stop", "kill"]
permission_classes = [AdminOnly]
def get(self, request, task_id):
"""handle get request"""
@@ -833,6 +962,8 @@ class RefreshView(ApiBaseView):
POST: start a manual refresh task
"""
permission_classes = [AdminOnly]
def get(self, request):
"""handle get request"""
request_type = request.GET.get("type")
@@ -859,6 +990,42 @@ class RefreshView(ApiBaseView):
return Response(data)
class UserConfigView(ApiBaseView):
"""resolves to /api/config/user/
GET: return current user config
POST: update user config
"""
def get(self, request):
"""get config"""
user_id = request.user.id
response = UserConfig(user_id).get_config()
response.update({"user_id": user_id})
return Response(response)
def post(self, request):
"""update config"""
user_id = request.user.id
data = request.data
user_conf = UserConfig(user_id)
for key, value in data.items():
try:
user_conf.set_value(key, value)
except ValueError as err:
message = {
"status": "Bad Request",
"message": f"failed updating {key} to '{value}', {err}",
}
return Response(message, status=400)
response = user_conf.get_config()
response.update({"user_id": user_id})
return Response(response)
class CookieView(ApiBaseView):
"""resolves to /api/cookie/
GET: check if cookie is enabled
@@ -866,6 +1033,8 @@ class CookieView(ApiBaseView):
PUT: import cookie
"""
permission_classes = [AdminOnly]
@staticmethod
def get(request):
"""handle get request"""
@@ -953,6 +1122,8 @@ class TokenView(ApiBaseView):
DELETE: revoke the token
"""
permission_classes = [AdminOnly]
@staticmethod
def delete(request):
print("revoke API token")
@@ -978,16 +1149,52 @@ class NotificationView(ApiBaseView):
return Response(RedisArchivist().list_items(query))
class StatPrimaryView(ApiBaseView):
"""resolves to /api/stats/primary/
GET: return document count
class StatVideoView(ApiBaseView):
"""resolves to /api/stats/video/
GET: return video stats
"""
def get(self, request):
"""get stats"""
# pylint: disable=unused-argument
return Response(Primary().process())
return Response(Video().process())
class StatChannelView(ApiBaseView):
"""resolves to /api/stats/channel/
GET: return channel stats
"""
def get(self, request):
"""get stats"""
# pylint: disable=unused-argument
return Response(Channel().process())
class StatPlaylistView(ApiBaseView):
"""resolves to /api/stats/playlist/
GET: return playlist stats
"""
def get(self, request):
"""get stats"""
# pylint: disable=unused-argument
return Response(Playlist().process())
class StatDownloadView(ApiBaseView):
"""resolves to /api/stats/download/
GET: return download stats
"""
def get(self, request):
"""get stats"""
# pylint: disable=unused-argument
return Response(Download().process())
class StatWatchProgress(ApiBaseView):

View File

@@ -3,12 +3,12 @@ Functionality:
- check that all connections are working
"""
from os import environ
from time import sleep
import requests
from django.core.management.base import BaseCommand, CommandError
from home.src.es.connect import ElasticWrap
from home.src.ta.settings import EnvironmentSettings
from home.src.ta.ta_redis import RedisArchivist
TOPIC = """
@@ -156,10 +156,8 @@ class Command(BaseCommand):
" 🗙 path.repo env var not found. "
+ "set the following env var to the ES container:\n"
+ " path.repo="
+ environ.get(
"ES_SNAPSHOT_DIR", "/usr/share/elasticsearch/data/snapshot"
),
+ EnvironmentSettings.ES_SNAPSHOT_DIR
)
self.stdout.write(self.style.ERROR(f"{message}"))
self.stdout.write(self.style.ERROR(message))
sleep(60)
raise CommandError(message)

View File

@@ -11,6 +11,7 @@ import re
from django.core.management.base import BaseCommand, CommandError
from home.models import Account
from home.src.ta.settings import EnvironmentSettings
LOGO = """
@@ -96,18 +97,14 @@ class Command(BaseCommand):
def _elastic_user_overwrite(self):
"""check for ELASTIC_USER overwrite"""
self.stdout.write("[2] set default ES user")
if not os.environ.get("ELASTIC_USER"):
os.environ.setdefault("ELASTIC_USER", "elastic")
env = os.environ.get("ELASTIC_USER")
self.stdout.write("[2] check ES user overwrite")
env = EnvironmentSettings.ES_USER
self.stdout.write(self.style.SUCCESS(f" ✓ ES user is set to {env}"))
def _ta_port_overwrite(self):
"""set TA_PORT overwrite for nginx"""
self.stdout.write("[3] check TA_PORT overwrite")
overwrite = os.environ.get("TA_PORT")
overwrite = EnvironmentSettings.TA_PORT
if not overwrite:
self.stdout.write(self.style.SUCCESS(" TA_PORT is not set"))
return
@@ -125,7 +122,7 @@ class Command(BaseCommand):
def _ta_uwsgi_overwrite(self):
"""set TA_UWSGI_PORT overwrite"""
self.stdout.write("[4] check TA_UWSGI_PORT overwrite")
overwrite = os.environ.get("TA_UWSGI_PORT")
overwrite = EnvironmentSettings.TA_UWSGI_PORT
if not overwrite:
message = " TA_UWSGI_PORT is not set"
self.stdout.write(self.style.SUCCESS(message))
@@ -151,7 +148,7 @@ class Command(BaseCommand):
def _enable_cast_overwrite(self):
"""cast workaround, remove auth for static files in nginx"""
self.stdout.write("[5] check ENABLE_CAST overwrite")
overwrite = os.environ.get("ENABLE_CAST")
overwrite = EnvironmentSettings.ENABLE_CAST
if not overwrite:
self.stdout.write(self.style.SUCCESS(" ENABLE_CAST is not set"))
return
@@ -174,8 +171,8 @@ class Command(BaseCommand):
self.stdout.write(self.style.SUCCESS(message))
return
name = os.environ.get("TA_USERNAME")
password = os.environ.get("TA_PASSWORD")
name = EnvironmentSettings.TA_USERNAME
password = EnvironmentSettings.TA_PASSWORD
Account.objects.create_superuser(name, password)
message = f" ✓ new superuser with name {name} created"
self.stdout.write(self.style.SUCCESS(message))

View File

@@ -6,8 +6,8 @@ import shutil
from django.core.management.base import BaseCommand
from home.src.es.connect import ElasticWrap, IndexPaginate
from home.src.ta.config import AppConfig
from home.src.ta.helper import ignore_filelist
from home.src.ta.settings import EnvironmentSettings
TOPIC = """
@@ -58,8 +58,7 @@ class FolderMigration:
"""migrate video archive folder"""
def __init__(self):
self.config = AppConfig().config
self.videos = self.config["application"]["videos"]
self.videos = EnvironmentSettings.MEDIA_DIR
self.bulk_list = []
def get_to_migrate(self):
@@ -84,8 +83,8 @@ class FolderMigration:
def create_folders(self, to_migrate):
"""create required channel folders"""
host_uid = self.config["application"]["HOST_UID"]
host_gid = self.config["application"]["HOST_GID"]
host_uid = EnvironmentSettings.HOST_UID
host_gid = EnvironmentSettings.HOST_GID
all_channel_ids = {i["channel"]["channel_id"] for i in to_migrate}
for channel_id in all_channel_ids:

View File

@@ -8,12 +8,11 @@ import os
from time import sleep
from django.core.management.base import BaseCommand, CommandError
from home.src.es.connect import ElasticWrap, IndexPaginate
from home.src.es.index_setup import ElasitIndexWrap
from home.src.es.snapshot import ElasticSnapshot
from home.src.index.video_streams import MediaStreamExtractor
from home.src.ta.config import AppConfig, ReleaseVersion
from home.src.ta.helper import clear_dl_cache
from home.src.ta.settings import EnvironmentSettings
from home.src.ta.ta_redis import RedisArchivist
from home.src.ta.task_manager import TaskManager
from home.src.ta.users import UserConfig
@@ -37,14 +36,12 @@ class Command(BaseCommand):
self.stdout.write(TOPIC)
self._sync_redis_state()
self._make_folders()
self._release_locks()
self._clear_redis_keys()
self._clear_tasks()
self._clear_dl_cache()
self._version_check()
self._mig_index_setup()
self._mig_snapshot_check()
self._mig_set_streams()
self._mig_set_autostart()
self._mig_move_users_to_es()
def _sync_redis_state(self):
@@ -69,17 +66,17 @@ class Command(BaseCommand):
"playlists",
"videos",
]
cache_dir = AppConfig().config["application"]["cache_dir"]
cache_dir = EnvironmentSettings.CACHE_DIR
for folder in folders:
folder_path = os.path.join(cache_dir, folder)
os.makedirs(folder_path, exist_ok=True)
self.stdout.write(self.style.SUCCESS(" ✓ expected folders created"))
def _release_locks(self):
"""make sure there are no leftover locks set in redis"""
self.stdout.write("[3] clear leftover locks in redis")
all_locks = [
def _clear_redis_keys(self):
"""make sure there are no leftover locks or keys set in redis"""
self.stdout.write("[3] clear leftover keys in redis")
all_keys = [
"dl_queue_id",
"dl_queue",
"downloading",
@@ -88,19 +85,22 @@ class Command(BaseCommand):
"rescan",
"run_backup",
"startup_check",
"reindex:ta_video",
"reindex:ta_channel",
"reindex:ta_playlist",
]
redis_con = RedisArchivist()
has_changed = False
for lock in all_locks:
if redis_con.del_message(lock):
for key in all_keys:
if redis_con.del_message(key):
self.stdout.write(
self.style.SUCCESS(f" ✓ cleared lock {lock}")
self.style.SUCCESS(f" ✓ cleared key {key}")
)
has_changed = True
if not has_changed:
self.stdout.write(self.style.SUCCESS(" no locks found"))
self.stdout.write(self.style.SUCCESS(" no keys found"))
def _clear_tasks(self):
"""clear tasks and messages"""
@@ -119,8 +119,7 @@ class Command(BaseCommand):
def _clear_dl_cache(self):
"""clear leftover files from dl cache"""
self.stdout.write("[5] clear leftover files from dl cache")
config = AppConfig().config
leftover_files = clear_dl_cache(config)
leftover_files = clear_dl_cache(EnvironmentSettings.CACHE_DIR)
if leftover_files:
self.stdout.write(
self.style.SUCCESS(f" ✓ cleared {leftover_files} files")
@@ -149,79 +148,6 @@ class Command(BaseCommand):
self.stdout.write("[MIGRATION] setup snapshots")
ElasticSnapshot().setup()
def _mig_set_streams(self):
"""migration: update from 0.3.5 to 0.3.6, set streams and media_size"""
self.stdout.write("[MIGRATION] index streams and media size")
videos = AppConfig().config["application"]["videos"]
data = {
"query": {
"bool": {"must_not": [{"exists": {"field": "streams"}}]}
},
"_source": ["media_url", "youtube_id"],
}
all_missing = IndexPaginate("ta_video", data).get_results()
if not all_missing:
self.stdout.write(" no videos need updating")
return
total = len(all_missing)
for idx, missing in enumerate(all_missing):
media_url = missing["media_url"]
youtube_id = missing["youtube_id"]
media_path = os.path.join(videos, media_url)
if not os.path.exists(media_path):
self.stdout.write(f" file not found: {media_path}")
self.stdout.write(" run file system rescan to fix")
continue
media = MediaStreamExtractor(media_path)
vid_data = {
"doc": {
"streams": media.extract_metadata(),
"media_size": media.get_file_size(),
}
}
path = f"ta_video/_update/{youtube_id}"
response, status_code = ElasticWrap(path).post(data=vid_data)
if not status_code == 200:
self.stdout.errors(
f" update failed: {path}, {response}, {status_code}"
)
if idx % 100 == 0:
self.stdout.write(f" progress {idx}/{total}")
def _mig_set_autostart(self):
"""migration: update from 0.3.5 to 0.3.6 set auto_start to false"""
self.stdout.write("[MIGRATION] set default download auto_start")
data = {
"query": {
"bool": {"must_not": [{"exists": {"field": "auto_start"}}]}
},
"script": {"source": "ctx._source['auto_start'] = false"},
}
path = "ta_download/_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} videos updated in ta_download"
)
)
else:
self.stdout.write(
" no videos needed updating in ta_download"
)
return
message = " 🗙 ta_download auto_start update failed"
self.stdout.write(self.style.ERROR(message))
self.stdout.write(response)
sleep(60)
raise CommandError(message)
def _mig_move_users_to_es(self): # noqa: C901
"""migration: update from 0.4.1 to 0.4.2 move user config to ES"""
self.stdout.write("[MIGRATION] move user configuration to ES")
@@ -239,11 +165,11 @@ class Command(BaseCommand):
for user in users:
new_conf = UserConfig(user)
colors_key = f"{user}:colors"
colors = redis.get_message(colors_key).get("status")
if colors:
new_conf.set_value("colors", colors)
redis.del_message(colors_key)
stylesheet_key = f"{user}:color"
stylesheet = redis.get_message(stylesheet_key).get("status")
if stylesheet:
new_conf.set_value("stylesheet", stylesheet)
redis.del_message(stylesheet_key)
sort_by_key = f"{user}:sort_by"
sort_by = redis.get_message(sort_by_key).get("status")
@@ -299,12 +225,12 @@ class Command(BaseCommand):
f" ✓ Settings for user '{user}' migrated to ES"
)
)
except Exception as e:
except Exception as err:
message = " 🗙 user migration to ES failed"
self.stdout.write(self.style.ERROR(message))
self.stdout.write(self.style.ERROR(e))
self.stdout.write(self.style.ERROR(err))
sleep(60)
raise CommandError(message)
raise CommandError(message) from err
else:
self.stdout.write(
self.style.SUCCESS(

View File

@@ -17,8 +17,8 @@ from pathlib import Path
import ldap
from corsheaders.defaults import default_headers
from django_auth_ldap.config import LDAPSearch
from home.src.ta.config import AppConfig
from home.src.ta.helper import ta_host_parser
from home.src.ta.settings import EnvironmentSettings
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
@@ -27,7 +27,7 @@ BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
PW_HASH = hashlib.sha256(environ["TA_PASSWORD"].encode())
PW_HASH = hashlib.sha256(EnvironmentSettings.TA_PASSWORD.encode())
SECRET_KEY = PW_HASH.hexdigest()
# SECURITY WARNING: don't run with debug turned on in production!
@@ -180,7 +180,7 @@ if bool(environ.get("TA_LDAP")):
# Database
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
CACHE_DIR = AppConfig().config["application"]["cache_dir"]
CACHE_DIR = EnvironmentSettings.CACHE_DIR
DB_PATH = path.join(CACHE_DIR, "db.sqlite3")
DATABASES = {
"default": {
@@ -228,7 +228,7 @@ if bool(environ.get("TA_ENABLE_AUTH_PROXY")):
# https://docs.djangoproject.com/en/3.2/topics/i18n/
LANGUAGE_CODE = "en-us"
TIME_ZONE = environ.get("TZ") or "UTC"
TIME_ZONE = EnvironmentSettings.TZ
USE_I18N = True
USE_L10N = True
USE_TZ = True
@@ -269,4 +269,4 @@ CORS_ALLOW_HEADERS = list(default_headers) + [
# TA application settings
TA_UPSTREAM = "https://github.com/tubearchivist/tubearchivist"
TA_VERSION = "v0.4.2"
TA_VERSION = "v0.4.5"

View File

@@ -25,10 +25,6 @@
"integrate_sponsorblock": false
},
"application": {
"app_root": "/app",
"cache_dir": "/cache",
"videos": "/youtube",
"enable_cast": false,
"enable_snapshot": true
},
"scheduler": {

View File

@@ -11,7 +11,7 @@ from time import sleep
import requests
from home.src.es.connect import ElasticWrap, IndexPaginate
from home.src.ta.config import AppConfig
from home.src.ta.settings import EnvironmentSettings
from mutagen.mp4 import MP4, MP4Cover
from PIL import Image, ImageFile, ImageFilter, UnidentifiedImageError
@@ -21,8 +21,7 @@ ImageFile.LOAD_TRUNCATED_IMAGES = True
class ThumbManagerBase:
"""base class for thumbnail management"""
CONFIG = AppConfig().config
CACHE_DIR = CONFIG["application"]["cache_dir"]
CACHE_DIR = EnvironmentSettings.CACHE_DIR
VIDEO_DIR = os.path.join(CACHE_DIR, "videos")
CHANNEL_DIR = os.path.join(CACHE_DIR, "channels")
PLAYLIST_DIR = os.path.join(CACHE_DIR, "playlists")
@@ -70,7 +69,7 @@ class ThumbManagerBase:
img_raw = Image.open(self.fallback)
return img_raw
app_root = self.CONFIG["application"]["app_root"]
app_root = EnvironmentSettings.APP_DIR
default_map = {
"video": os.path.join(
app_root, "static/img/default-video-thumb.jpg"
@@ -380,9 +379,8 @@ class ThumbFilesystem:
class EmbedCallback:
"""callback class to embed thumbnails"""
CONFIG = AppConfig().config
CACHE_DIR = CONFIG["application"]["cache_dir"]
MEDIA_DIR = CONFIG["application"]["videos"]
CACHE_DIR = EnvironmentSettings.CACHE_DIR
MEDIA_DIR = EnvironmentSettings.MEDIA_DIR
FORMAT = MP4Cover.FORMAT_JPEG
def __init__(self, source, index_name, counter=0):

View File

@@ -10,6 +10,7 @@ from http import cookiejar
from io import StringIO
import yt_dlp
from home.src.ta.settings import EnvironmentSettings
from home.src.ta.ta_redis import RedisArchivist
@@ -86,6 +87,7 @@ class CookieHandler:
def __init__(self, config):
self.cookie_io = False
self.config = config
self.cache_dir = EnvironmentSettings.CACHE_DIR
def get(self):
"""get cookie io stream"""
@@ -95,8 +97,9 @@ class CookieHandler:
def import_cookie(self):
"""import cookie from file"""
cache_path = self.config["application"]["cache_dir"]
import_path = os.path.join(cache_path, "import", "cookies.google.txt")
import_path = os.path.join(
self.cache_dir, "import", "cookies.google.txt"
)
try:
with open(import_path, encoding="utf-8") as cookie_file:

View File

@@ -21,6 +21,7 @@ from home.src.index.video import YoutubeVideo, index_new_video
from home.src.index.video_constants import VideoTypeEnum
from home.src.ta.config import AppConfig
from home.src.ta.helper import ignore_filelist
from home.src.ta.settings import EnvironmentSettings
class DownloadPostProcess:
@@ -49,7 +50,7 @@ class DownloadPostProcess:
return
print(f"auto delete older than {autodelete_days} days")
now_lte = self.now - autodelete_days * 24 * 60 * 60
now_lte = str(self.now - autodelete_days * 24 * 60 * 60)
data = {
"query": {"range": {"player.watched_date": {"lte": now_lte}}},
"sort": [{"player.watched_date": {"order": "asc"}}],
@@ -62,7 +63,7 @@ class DownloadPostProcess:
if "autodelete_days" in value:
autodelete_days = value.get("autodelete_days")
print(f"{channel_id}: delete older than {autodelete_days}d")
now_lte = self.now - autodelete_days * 24 * 60 * 60
now_lte = str(self.now - autodelete_days * 24 * 60 * 60)
must_list = [
{"range": {"player.watched_date": {"lte": now_lte}}},
{"term": {"channel.channel_id": {"value": channel_id}}},
@@ -153,6 +154,8 @@ class VideoDownloader:
self.youtube_id_list = youtube_id_list
self.task = task
self.config = AppConfig().config
self.cache_dir = EnvironmentSettings.CACHE_DIR
self.media_dir = EnvironmentSettings.MEDIA_DIR
self._build_obs()
self.channels = set()
self.videos = set()
@@ -262,10 +265,7 @@ class VideoDownloader:
"""initial obs"""
self.obs = {
"merge_output_format": "mp4",
"outtmpl": (
self.config["application"]["cache_dir"]
+ "/download/%(id)s.mp4"
),
"outtmpl": (self.cache_dir + "/download/%(id)s.mp4"),
"progress_hooks": [self._progress_hook],
"noprogress": True,
"continuedl": True,
@@ -340,7 +340,7 @@ class VideoDownloader:
if format_overwrite:
obs["format"] = format_overwrite
dl_cache = self.config["application"]["cache_dir"] + "/download/"
dl_cache = self.cache_dir + "/download/"
# check if already in cache to continue from there
all_cached = ignore_filelist(os.listdir(dl_cache))
@@ -370,20 +370,20 @@ class VideoDownloader:
def move_to_archive(self, vid_dict):
"""move downloaded video from cache to archive"""
videos = self.config["application"]["videos"]
host_uid = self.config["application"]["HOST_UID"]
host_gid = self.config["application"]["HOST_GID"]
host_uid = EnvironmentSettings.HOST_UID
host_gid = EnvironmentSettings.HOST_GID
# make folder
folder = os.path.join(videos, vid_dict["channel"]["channel_id"])
folder = os.path.join(
self.media_dir, vid_dict["channel"]["channel_id"]
)
if not os.path.exists(folder):
os.makedirs(folder)
if host_uid and host_gid:
os.chown(folder, host_uid, host_gid)
# move media file
media_file = vid_dict["youtube_id"] + ".mp4"
cache_dir = self.config["application"]["cache_dir"]
old_path = os.path.join(cache_dir, "download", media_file)
new_path = os.path.join(videos, vid_dict["media_url"])
old_path = os.path.join(self.cache_dir, "download", media_file)
new_path = os.path.join(self.media_dir, vid_dict["media_url"])
# move media file and fix permission
shutil.move(old_path, new_path, copy_function=shutil.copyfile)
if host_uid and host_gid:

View File

@@ -13,16 +13,18 @@ from datetime import datetime
from home.src.es.connect import ElasticWrap, IndexPaginate
from home.src.ta.config import AppConfig
from home.src.ta.helper import get_mapping, ignore_filelist
from home.src.ta.settings import EnvironmentSettings
class ElasticBackup:
"""dump index to nd-json files for later bulk import"""
INDEX_SPLIT = ["comment"]
CACHE_DIR = EnvironmentSettings.CACHE_DIR
BACKUP_DIR = os.path.join(CACHE_DIR, "backup")
def __init__(self, reason=False, task=False):
self.config = AppConfig().config
self.cache_dir = self.config["application"]["cache_dir"]
self.timestamp = datetime.now().strftime("%Y%m%d")
self.index_config = get_mapping()
self.reason = reason
@@ -78,14 +80,13 @@ class ElasticBackup:
def zip_it(self):
"""pack it up into single zip file"""
file_name = f"ta_backup-{self.timestamp}-{self.reason}.zip"
folder = os.path.join(self.cache_dir, "backup")
to_backup = []
for file in os.listdir(folder):
for file in os.listdir(self.BACKUP_DIR):
if file.endswith(".json"):
to_backup.append(os.path.join(folder, file))
to_backup.append(os.path.join(self.BACKUP_DIR, file))
backup_file = os.path.join(folder, file_name)
backup_file = os.path.join(self.BACKUP_DIR, file_name)
comp = zipfile.ZIP_DEFLATED
with zipfile.ZipFile(backup_file, "w", compression=comp) as zip_f:
@@ -98,7 +99,7 @@ class ElasticBackup:
def post_bulk_restore(self, file_name):
"""send bulk to es"""
file_path = os.path.join(self.cache_dir, file_name)
file_path = os.path.join(self.CACHE_DIR, file_name)
with open(file_path, "r", encoding="utf-8") as f:
data = f.read()
@@ -109,9 +110,7 @@ class ElasticBackup:
def get_all_backup_files(self):
"""build all available backup files for view"""
backup_dir = os.path.join(self.cache_dir, "backup")
backup_files = os.listdir(backup_dir)
all_backup_files = ignore_filelist(backup_files)
all_backup_files = ignore_filelist(os.listdir(self.BACKUP_DIR))
all_available_backups = [
i
for i in all_backup_files
@@ -120,24 +119,36 @@ class ElasticBackup:
all_available_backups.sort(reverse=True)
backup_dicts = []
for backup_file in all_available_backups:
file_split = backup_file.split("-")
if len(file_split) == 2:
timestamp = file_split[1].strip(".zip")
reason = False
elif len(file_split) == 3:
timestamp = file_split[1]
reason = file_split[2].strip(".zip")
to_add = {
"filename": backup_file,
"timestamp": timestamp,
"reason": reason,
}
backup_dicts.append(to_add)
for filename in all_available_backups:
data = self.build_backup_file_data(filename)
backup_dicts.append(data)
return backup_dicts
def build_backup_file_data(self, filename):
"""build metadata of single backup file"""
file_path = os.path.join(self.BACKUP_DIR, filename)
if not os.path.exists(file_path):
return False
file_split = filename.split("-")
if len(file_split) == 2:
timestamp = file_split[1].strip(".zip")
reason = False
elif len(file_split) == 3:
timestamp = file_split[1]
reason = file_split[2].strip(".zip")
data = {
"filename": filename,
"file_path": file_path,
"file_size": os.path.getsize(file_path),
"timestamp": timestamp,
"reason": reason,
}
return data
def restore(self, filename):
"""
restore from backup zip file
@@ -148,22 +159,19 @@ class ElasticBackup:
def _unpack_zip_backup(self, filename):
"""extract backup zip and return filelist"""
backup_dir = os.path.join(self.cache_dir, "backup")
file_path = os.path.join(backup_dir, filename)
file_path = os.path.join(self.BACKUP_DIR, filename)
with zipfile.ZipFile(file_path, "r") as z:
zip_content = z.namelist()
z.extractall(backup_dir)
z.extractall(self.BACKUP_DIR)
return zip_content
def _restore_json_files(self, zip_content):
"""go through the unpacked files and restore"""
backup_dir = os.path.join(self.cache_dir, "backup")
for idx, json_f in enumerate(zip_content):
self._notify_restore(idx, json_f, len(zip_content))
file_name = os.path.join(backup_dir, json_f)
file_name = os.path.join(self.BACKUP_DIR, json_f)
if not json_f.startswith("es_") or not json_f.endswith(".json"):
os.remove(file_name)
@@ -200,13 +208,21 @@ class ElasticBackup:
print("no backup files to rotate")
return
backup_dir = os.path.join(self.cache_dir, "backup")
all_to_delete = auto[rotate:]
for to_delete in all_to_delete:
file_path = os.path.join(backup_dir, to_delete["filename"])
print(f"remove old backup file: {file_path}")
os.remove(file_path)
self.delete_file(to_delete["filename"])
def delete_file(self, filename):
"""delete backup file"""
file_path = os.path.join(self.BACKUP_DIR, filename)
if not os.path.exists(file_path):
print(f"backup file not found: {filename}")
return False
print(f"remove old backup file: {file_path}")
os.remove(file_path)
return file_path
class BackupCallback:
@@ -217,6 +233,7 @@ class BackupCallback:
self.index_name = index_name
self.counter = counter
self.timestamp = datetime.now().strftime("%Y%m%d")
self.cache_dir = EnvironmentSettings.CACHE_DIR
def run(self):
"""run the junk task"""
@@ -243,9 +260,8 @@ class BackupCallback:
def _write_es_json(self, file_content):
"""write nd-json file for es _bulk API to disk"""
cache_dir = AppConfig().config["application"]["cache_dir"]
index = self.index_name.lstrip("ta_")
file_name = f"es_{index}-{self.timestamp}-{self.counter}.json"
file_path = os.path.join(cache_dir, "backup", file_name)
file_path = os.path.join(self.cache_dir, "backup", file_name)
with open(file_path, "a+", encoding="utf-8") as f:
f.write(file_content)

View File

@@ -6,11 +6,11 @@ functionality:
# pylint: disable=missing-timeout
import json
import os
from typing import Any
import requests
import urllib3
from home.src.ta.settings import EnvironmentSettings
class ElasticWrap:
@@ -18,16 +18,14 @@ class ElasticWrap:
returns response json and status code tuple
"""
ES_URL: str = str(os.environ.get("ES_URL"))
ES_PASS: str = str(os.environ.get("ELASTIC_PASSWORD"))
ES_USER: str = str(os.environ.get("ELASTIC_USER") or "elastic")
ES_DISABLE_VERIFY_SSL: bool = bool(os.environ.get("ES_DISABLE_VERIFY_SSL"))
def __init__(self, path: str):
self.url: str = f"{self.ES_URL}/{path}"
self.auth: tuple[str, str] = (self.ES_USER, self.ES_PASS)
self.url: str = f"{EnvironmentSettings.ES_URL}/{path}"
self.auth: tuple[str, str] = (
EnvironmentSettings.ES_USER,
EnvironmentSettings.ES_PASS,
)
if self.ES_DISABLE_VERIFY_SSL:
if EnvironmentSettings.ES_DISABLE_VERIFY_SSL:
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
def get(
@@ -43,7 +41,7 @@ class ElasticWrap:
"timeout": timeout,
}
if self.ES_DISABLE_VERIFY_SSL:
if EnvironmentSettings.ES_DISABLE_VERIFY_SSL:
kwargs["verify"] = False
if data:
@@ -78,7 +76,7 @@ class ElasticWrap:
}
)
if self.ES_DISABLE_VERIFY_SSL:
if EnvironmentSettings.ES_DISABLE_VERIFY_SSL:
kwargs["verify"] = False
response = requests.post(self.url, **kwargs)
@@ -103,7 +101,7 @@ class ElasticWrap:
"auth": self.auth,
}
if self.ES_DISABLE_VERIFY_SSL:
if EnvironmentSettings.ES_DISABLE_VERIFY_SSL:
kwargs["verify"] = False
response = requests.put(self.url, **kwargs)
@@ -130,7 +128,7 @@ class ElasticWrap:
if data:
kwargs["json"] = data
if self.ES_DISABLE_VERIFY_SSL:
if EnvironmentSettings.ES_DISABLE_VERIFY_SSL:
kwargs["verify"] = False
response = requests.delete(self.url, **kwargs)

View File

@@ -74,7 +74,7 @@
"type": "boolean"
},
"integrate_sponsorblock": {
"type" : "boolean"
"type": "boolean"
}
}
}
@@ -168,7 +168,7 @@
"type": "boolean"
},
"integrate_sponsorblock": {
"type" : "boolean"
"type": "boolean"
}
}
}
@@ -236,19 +236,37 @@
"comment_count": {
"type": "long"
},
"stats" : {
"properties" : {
"average_rating" : {
"type" : "float"
"stats": {
"properties": {
"average_rating": {
"type": "float"
},
"dislike_count" : {
"type" : "long"
"dislike_count": {
"type": "long"
},
"like_count" : {
"type" : "long"
"like_count": {
"type": "long"
},
"view_count" : {
"type" : "long"
"view_count": {
"type": "long"
}
}
},
"player": {
"properties": {
"duration": {
"type": "long"
},
"duration_str": {
"type": "keyword",
"index": false
},
"watched": {
"type": "boolean"
},
"watched_date": {
"type": "date",
"format": "epoch_second"
}
}
},
@@ -314,28 +332,28 @@
"is_enabled": {
"type": "boolean"
},
"segments" : {
"properties" : {
"UUID" : {
"segments": {
"properties": {
"UUID": {
"type": "keyword"
},
"actionType" : {
"actionType": {
"type": "keyword"
},
"category" : {
"category": {
"type": "keyword"
},
"locked" : {
"type" : "short"
"locked": {
"type": "short"
},
"segment" : {
"type" : "float"
"segment": {
"type": "float"
},
"videoDuration" : {
"type" : "float"
"videoDuration": {
"type": "float"
},
"votes" : {
"type" : "long"
"votes": {
"type": "long"
}
}
}
@@ -459,6 +477,41 @@
"playlist_last_refresh": {
"type": "date",
"format": "epoch_second"
},
"playlist_entries": {
"properties": {
"downloaded": {
"type": "boolean"
},
"idx": {
"type": "long"
},
"title": {
"type": "text",
"analyzer": "english",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256,
"normalizer": "to_lower"
}
}
},
"uploader": {
"type": "text",
"analyzer": "english",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256,
"normalizer": "to_lower"
}
}
},
"youtube_id": {
"type": "keyword"
}
}
}
},
"expected_set": {
@@ -516,7 +569,7 @@
"format": "epoch_second"
},
"subtitle_index": {
"type" : "long"
"type": "long"
},
"subtitle_lang": {
"type": "keyword"
@@ -525,7 +578,7 @@
"type": "keyword"
},
"subtitle_line": {
"type" : "text",
"type": "text",
"analyzer": "english"
}
},
@@ -560,14 +613,14 @@
"type": "keyword"
},
"comment_text": {
"type" : "text"
"type": "text"
},
"comment_timestamp": {
"type": "date",
"format": "epoch_second"
},
"comment_time_text": {
"type" : "text"
"type": "text"
},
"comment_likecount": {
"type": "long"
@@ -613,4 +666,4 @@
}
}
]
}
}

View File

@@ -4,12 +4,12 @@ functionality:
"""
from datetime import datetime
from os import environ
from time import sleep
from zoneinfo import ZoneInfo
from home.src.es.connect import ElasticWrap
from home.src.ta.helper import get_mapping
from home.src.ta.settings import EnvironmentSettings
class ElasticSnapshot:
@@ -19,9 +19,7 @@ class ElasticSnapshot:
REPO_SETTINGS = {
"compress": "true",
"chunk_size": "1g",
"location": environ.get(
"ES_SNAPSHOT_DIR", "/usr/share/elasticsearch/data/snapshot"
),
"location": EnvironmentSettings.ES_SNAPSHOT_DIR,
}
POLICY = "ta_daily"
@@ -256,7 +254,7 @@ class ElasticSnapshot:
expected_format = "%Y-%m-%dT%H:%M:%S.%fZ"
date = datetime.strptime(date_utc, expected_format)
local_datetime = date.replace(tzinfo=ZoneInfo("localtime"))
converted = local_datetime.astimezone(ZoneInfo(environ.get("TZ")))
converted = local_datetime.astimezone(ZoneInfo(EnvironmentSettings.TZ))
converted_str = converted.strftime("%Y-%m-%d %H:%M")
return converted_str

View File

@@ -1,92 +0,0 @@
"""
Functionality:
- collection of functions and tasks from frontend
- called via user input
"""
from home.src.ta.users import UserConfig
from home.tasks import run_restore_backup
class PostData:
"""
map frontend http post values to backend funcs
handover long running tasks to celery
"""
def __init__(self, post_dict, current_user):
self.post_dict = post_dict
self.to_exec, self.exec_val = list(post_dict.items())[0]
self.current_user = current_user
def run_task(self):
"""execute and return task result"""
to_exec = self.exec_map()
task_result = to_exec()
return task_result
def exec_map(self):
"""map dict key and return function to execute"""
exec_map = {
"change_view": self._change_view,
"change_grid": self._change_grid,
"sort_order": self._sort_order,
"hide_watched": self._hide_watched,
"show_subed_only": self._show_subed_only,
"show_ignored_only": self._show_ignored_only,
"db-restore": self._db_restore,
}
return exec_map[self.to_exec]
def _change_view(self):
"""process view changes in home, channel, and downloads"""
view, setting = self.exec_val.split(":")
UserConfig(self.current_user).set_value(f"view_style_{view}", setting)
return {"success": True}
def _change_grid(self):
"""process change items in grid"""
grid_items = int(self.exec_val)
grid_items = max(grid_items, 3)
grid_items = min(grid_items, 7)
UserConfig(self.current_user).set_value("grid_items", grid_items)
return {"success": True}
def _sort_order(self):
"""change the sort between published to downloaded"""
if self.exec_val in ["asc", "desc"]:
UserConfig(self.current_user).set_value(
"sort_order", self.exec_val
)
else:
UserConfig(self.current_user).set_value("sort_by", self.exec_val)
return {"success": True}
def _hide_watched(self):
"""toggle if to show watched vids or not"""
UserConfig(self.current_user).set_value(
"hide_watched", bool(int(self.exec_val))
)
return {"success": True}
def _show_subed_only(self):
"""show or hide subscribed channels only on channels page"""
UserConfig(self.current_user).set_value(
"show_subed_only", bool(int(self.exec_val))
)
return {"success": True}
def _show_ignored_only(self):
"""switch view on /downloads/ to show ignored only"""
UserConfig(self.current_user).set_value(
"show_ignored_only", bool(int(self.exec_val))
)
return {"success": True}
def _db_restore(self):
"""restore es zip from settings page"""
print("restoring index from backup zip")
filename = self.exec_val
run_restore_backup.delay(filename)
return {"success": True}

View File

@@ -2,9 +2,12 @@
- hold all form classes used in the views
"""
import os
from django import forms
from django.contrib.auth.forms import AuthenticationForm
from django.forms.widgets import PasswordInput, TextInput
from home.src.ta.helper import get_stylesheets
class CustomAuthForm(AuthenticationForm):
@@ -29,14 +32,16 @@ class CustomAuthForm(AuthenticationForm):
class UserSettingsForm(forms.Form):
"""user configurations values"""
CHOICES = [
("", "-- change color scheme --"),
("dark", "Dark"),
("light", "Light"),
]
STYLESHEET_CHOICES = [("", "-- change stylesheet --")]
STYLESHEET_CHOICES.extend(
[
(stylesheet, os.path.splitext(stylesheet)[0].title())
for stylesheet in get_stylesheets()
]
)
colors = forms.ChoiceField(
widget=forms.Select, choices=CHOICES, required=False
stylesheet = forms.ChoiceField(
widget=forms.Select, choices=STYLESHEET_CHOICES, required=False
)
page_size = forms.IntegerField(required=False)

View File

@@ -14,6 +14,7 @@ from home.src.download.yt_dlp_base import YtWrap
from home.src.es.connect import ElasticWrap, IndexPaginate
from home.src.index.generic import YouTubeItem
from home.src.index.playlist import YoutubePlaylist
from home.src.ta.settings import EnvironmentSettings
class YoutubeChannel(YouTubeItem):
@@ -22,10 +23,7 @@ class YoutubeChannel(YouTubeItem):
es_path = False
index_name = "ta_channel"
yt_base = "https://www.youtube.com/channel/"
yt_obs = {
"extract_flat": True,
"allow_playlist_files": True,
}
yt_obs = {"playlist_items": "0,0"}
def __init__(self, youtube_id, task=False):
super().__init__(youtube_id)
@@ -68,7 +66,7 @@ class YoutubeChannel(YouTubeItem):
"channel_banner_url": self._get_banner_art(),
"channel_thumb_url": self._get_thumb_art(),
"channel_tvart_url": self._get_tv_art(),
"channel_views": self.youtube_meta.get("view_count", 0),
"channel_views": self.youtube_meta.get("view_count") or 0,
}
def _parse_tags(self, tags):
@@ -134,7 +132,7 @@ class YoutubeChannel(YouTubeItem):
def _info_json_fallback(self):
"""read channel info.json for additional metadata"""
info_json = os.path.join(
self.config["application"]["cache_dir"],
EnvironmentSettings.CACHE_DIR,
"import",
f"{self.youtube_id}.info.json",
)
@@ -178,7 +176,7 @@ class YoutubeChannel(YouTubeItem):
def get_folder_path(self):
"""get folder where media files get stored"""
folder_path = os.path.join(
self.app_conf["videos"],
EnvironmentSettings.MEDIA_DIR,
self.json_data["channel_id"],
)
return folder_path

View File

@@ -77,7 +77,7 @@ class Comments:
def get_yt_comments(self):
"""get comments from youtube"""
yt_obs = self.build_yt_obs()
info_json = YtWrap(yt_obs).extract(self.youtube_id)
info_json = YtWrap(yt_obs, config=self.config).extract(self.youtube_id)
if not info_json:
return False, False
@@ -126,7 +126,7 @@ class Comments:
"comment_author_id": comment["author_id"],
"comment_author_thumbnail": comment["author_thumbnail"],
"comment_author_is_uploader": comment.get(
"comment_author_is_uploader", False
"author_is_uploader", False
),
"comment_parent": comment["parent"],
}

View File

@@ -8,14 +8,14 @@ import os
from home.src.es.connect import ElasticWrap, IndexPaginate
from home.src.index.comments import CommentList
from home.src.index.video import YoutubeVideo, index_new_video
from home.src.ta.config import AppConfig
from home.src.ta.helper import ignore_filelist
from home.src.ta.settings import EnvironmentSettings
class Scanner:
"""scan index and filesystem"""
VIDEOS: str = AppConfig().config["application"]["videos"]
VIDEOS: str = EnvironmentSettings.MEDIA_DIR
def __init__(self, task=False) -> None:
self.task = task
@@ -83,7 +83,7 @@ class Scanner:
if self.task:
self.task.send_progress(
message_lines=[
f"Index missing video {youtube_id}, {idx}/{total}"
f"Index missing video {youtube_id}, {idx + 1}/{total}"
],
progress=(idx + 1) / total,
)

View File

@@ -26,7 +26,6 @@ class YouTubeItem:
self.youtube_id = youtube_id
self.es_path = f"{self.index_name}/_doc/{youtube_id}"
self.config = AppConfig().config
self.app_conf = self.config["application"]
self.youtube_meta = False
self.json_data = False

View File

@@ -16,6 +16,7 @@ from home.src.index.comments import CommentList
from home.src.index.video import YoutubeVideo
from home.src.ta.config import AppConfig
from home.src.ta.helper import ignore_filelist
from home.src.ta.settings import EnvironmentSettings
from PIL import Image
from yt_dlp.utils import ISO639Utils
@@ -28,7 +29,7 @@ class ImportFolderScanner:
"""
CONFIG = AppConfig().config
CACHE_DIR = CONFIG["application"]["cache_dir"]
CACHE_DIR = EnvironmentSettings.CACHE_DIR
IMPORT_DIR = os.path.join(CACHE_DIR, "import")
"""All extensions should be in lowercase until better handling is in place.
@@ -433,9 +434,9 @@ class ManualImport:
def _move_to_archive(self, json_data):
"""move identified media file to archive"""
videos = self.config["application"]["videos"]
host_uid = self.config["application"]["HOST_UID"]
host_gid = self.config["application"]["HOST_GID"]
videos = EnvironmentSettings.MEDIA_DIR
host_uid = EnvironmentSettings.HOST_UID
host_gid = EnvironmentSettings.HOST_GID
channel, file = os.path.split(json_data["media_url"])
channel_folder = os.path.join(videos, channel)
@@ -472,7 +473,7 @@ class ManualImport:
os.remove(subtitle_file)
channel_info = os.path.join(
self.config["application"]["cache_dir"],
EnvironmentSettings.CACHE_DIR,
"import",
f"{json_data['channel']['channel_id']}.info.json",
)

View File

@@ -19,6 +19,7 @@ from home.src.index.comments import Comments
from home.src.index.playlist import YoutubePlaylist
from home.src.index.video import YoutubeVideo
from home.src.ta.config import AppConfig
from home.src.ta.settings import EnvironmentSettings
from home.src.ta.ta_redis import RedisQueue
@@ -104,11 +105,13 @@ class ReindexPopulate(ReindexBase):
"""get total hits from index"""
index_name = reindex_config["index_name"]
active_key = reindex_config["active_key"]
path = f"{index_name}/_search?filter_path=hits.total"
data = {"query": {"match": {active_key: True}}}
response, _ = ElasticWrap(path).post(data=data)
total_hits = response["hits"]["total"]["value"]
return total_hits
data = {
"query": {"term": {active_key: {"value": True}}},
"_source": False,
}
total = IndexPaginate(index_name, data, keep_source=True).get_results()
return len(total)
def _get_daily_should(self, total_hits):
"""calc how many should reindex daily"""
@@ -122,7 +125,7 @@ class ReindexPopulate(ReindexBase):
"""get outdated from index_name"""
index_name = reindex_config["index_name"]
refresh_key = reindex_config["refresh_key"]
now_lte = self.now - self.interval * 24 * 60 * 60
now_lte = str(self.now - self.interval * 24 * 60 * 60)
must_list = [
{"match": {reindex_config["active_key"]: True}},
{"range": {refresh_key: {"lte": now_lte}}},
@@ -240,7 +243,7 @@ class Reindex(ReindexBase):
return
for name, index_config in self.REINDEX_CONFIG.items():
if not RedisQueue(index_config["queue_name"]).has_item():
if not RedisQueue(index_config["queue_name"]).length():
continue
self.total = RedisQueue(index_config["queue_name"]).length()
@@ -293,7 +296,7 @@ class Reindex(ReindexBase):
# get new
media_url = os.path.join(
self.config["application"]["videos"], es_meta["media_url"]
EnvironmentSettings.MEDIA_DIR, es_meta["media_url"]
)
video.build_json(media_path=media_url)
if not video.youtube_meta:
@@ -311,10 +314,6 @@ class Reindex(ReindexBase):
video.json_data["playlist"] = es_meta.get("playlist")
video.upload_to_es()
if es_meta.get("media_url") != video.json_data["media_url"]:
self._rename_media_file(
es_meta.get("media_url"), video.json_data["media_url"]
)
thumb_handler = ThumbManager(youtube_id)
thumb_handler.delete_video_thumb()
@@ -325,14 +324,6 @@ class Reindex(ReindexBase):
return
def _rename_media_file(self, media_url_is, media_url_should):
"""handle title change"""
print(f"[reindex] fix media_url {media_url_is} to {media_url_should}")
videos = self.config["application"]["videos"]
old_path = os.path.join(videos, media_url_is)
new_path = os.path.join(videos, media_url_should)
os.rename(old_path, new_path)
def _reindex_single_channel(self, channel_id):
"""refresh channel data and sync to videos"""
# read current state

View File

@@ -12,6 +12,7 @@ from datetime import datetime
import requests
from home.src.es.connect import ElasticWrap
from home.src.ta.helper import requests_headers
from home.src.ta.settings import EnvironmentSettings
class YoutubeSubtitle:
@@ -113,7 +114,7 @@ class YoutubeSubtitle:
def download_subtitles(self, relevant_subtitles):
"""download subtitle files to archive"""
videos_base = self.video.config["application"]["videos"]
videos_base = EnvironmentSettings.MEDIA_DIR
indexed = []
for subtitle in relevant_subtitles:
dest_path = os.path.join(videos_base, subtitle["media_url"])
@@ -149,8 +150,8 @@ class YoutubeSubtitle:
with open(dest_path, "w", encoding="utf-8") as subfile:
subfile.write(subtitle_str)
host_uid = self.video.config["application"]["HOST_UID"]
host_gid = self.video.config["application"]["HOST_GID"]
host_uid = EnvironmentSettings.HOST_UID
host_gid = EnvironmentSettings.HOST_GID
if host_uid and host_gid:
os.chown(dest_path, host_uid, host_gid)
@@ -162,7 +163,7 @@ class YoutubeSubtitle:
def delete(self, subtitles=False):
"""delete subtitles from index and filesystem"""
youtube_id = self.video.youtube_id
videos_base = self.video.config["application"]["videos"]
videos_base = EnvironmentSettings.MEDIA_DIR
# delete files
if subtitles:
files = [i["media_url"] for i in subtitles]

View File

@@ -18,6 +18,7 @@ from home.src.index.subtitle import YoutubeSubtitle
from home.src.index.video_constants import VideoTypeEnum
from home.src.index.video_streams import MediaStreamExtractor
from home.src.ta.helper import get_duration_sec, get_duration_str, randomizor
from home.src.ta.settings import EnvironmentSettings
from home.src.ta.users import UserConfig
from ryd_client import ryd_client
@@ -187,11 +188,11 @@ class YoutubeVideo(YouTubeItem, YoutubeSubtitle):
# build json_data basics
self.json_data = {
"title": self.youtube_meta["title"],
"description": self.youtube_meta["description"],
"category": self.youtube_meta["categories"],
"description": self.youtube_meta.get("description", ""),
"category": self.youtube_meta.get("categories", []),
"vid_thumb_url": self.youtube_meta["thumbnail"],
"vid_thumb_base64": base64_blur,
"tags": self.youtube_meta["tags"],
"tags": self.youtube_meta.get("tags", []),
"published": published,
"vid_last_refresh": last_refresh,
"date_downloaded": last_refresh,
@@ -209,31 +210,24 @@ class YoutubeVideo(YouTubeItem, YoutubeSubtitle):
def _add_stats(self):
"""add stats dicst to json_data"""
# likes
like_count = self.youtube_meta.get("like_count", 0)
dislike_count = self.youtube_meta.get("dislike_count", 0)
average_rating = self.youtube_meta.get("average_rating", 0)
self.json_data.update(
{
"stats": {
"view_count": self.youtube_meta["view_count"],
"like_count": like_count,
"dislike_count": dislike_count,
"average_rating": average_rating,
}
}
)
stats = {
"view_count": self.youtube_meta.get("view_count", 0),
"like_count": self.youtube_meta.get("like_count", 0),
"dislike_count": self.youtube_meta.get("dislike_count", 0),
"average_rating": self.youtube_meta.get("average_rating", 0),
}
self.json_data.update({"stats": stats})
def build_dl_cache_path(self):
"""find video path in dl cache"""
cache_dir = self.app_conf["cache_dir"]
cache_dir = EnvironmentSettings.CACHE_DIR
video_id = self.json_data["youtube_id"]
cache_path = f"{cache_dir}/download/{video_id}.mp4"
if os.path.exists(cache_path):
return cache_path
channel_path = os.path.join(
self.app_conf["videos"],
EnvironmentSettings.MEDIA_DIR,
self.json_data["channel"]["channel_id"],
f"{video_id}.mp4",
)
@@ -282,7 +276,7 @@ class YoutubeVideo(YouTubeItem, YoutubeSubtitle):
if not self.json_data:
raise FileNotFoundError
video_base = self.app_conf["videos"]
video_base = EnvironmentSettings.MEDIA_DIR
media_url = self.json_data.get("media_url")
file_path = os.path.join(video_base, media_url)
try:

View File

@@ -5,7 +5,6 @@ Functionality:
"""
import json
import os
import re
from random import randint
from time import sleep
@@ -28,7 +27,6 @@ class AppConfig:
if not config:
config = self.get_config_file()
config["application"].update(self.get_config_env())
return config
def get_config_file(self):
@@ -36,25 +34,8 @@ class AppConfig:
with open("home/config.json", "r", encoding="utf-8") as f:
config_file = json.load(f)
config_file["application"].update(self.get_config_env())
return config_file
@staticmethod
def get_config_env():
"""read environment application variables.
Connection to ES is managed in ElasticWrap and the
connection to Redis is managed in RedisArchivist."""
application = {
"HOST_UID": int(os.environ.get("HOST_UID", False)),
"HOST_GID": int(os.environ.get("HOST_GID", False)),
"enable_cast": bool(os.environ.get("ENABLE_CAST")),
}
return application
@staticmethod
def get_config_redis():
"""read config json set from redis to overwrite defaults"""
@@ -125,7 +106,10 @@ class AppConfig:
# missing nested values
for sub_key, sub_value in value.items():
if sub_key not in redis_config[key].keys():
if (
sub_key not in redis_config[key].keys()
or sub_value == "rand-d"
):
if sub_value == "rand-d":
sub_value = self._build_rand_daily()
@@ -296,12 +280,12 @@ class ReleaseVersion:
"""check version"""
print(f"[{self.local_version}]: look for updates")
self.get_remote_version()
new_version, is_breaking = self._has_update()
new_version = self._has_update()
if new_version:
message = {
"status": True,
"version": new_version,
"is_breaking": is_breaking,
"is_breaking": self.is_breaking,
}
RedisArchivist().set_message(self.NEW_KEY, message)
print(f"[{self.local_version}]: found new version {new_version}")
@@ -320,15 +304,13 @@ class ReleaseVersion:
def _has_update(self):
"""check if there is an update"""
for idx, number in enumerate(self.local_version):
is_newer = self.remote_version[idx] > number
if is_newer:
return self.response["release_version"], self.is_breaking
if self.remote_version > self.local_version:
return self.remote_version
if self.is_unstable and self.local_version == self.remote_version:
return self.response["release_version"], self.is_breaking
return self.remote_version
return False, False
return False
@staticmethod
def _parse_version(version):
@@ -342,7 +324,7 @@ class ReleaseVersion:
if not message:
return False
if self._parse_version(message.get("version")) == self.local_version:
if self.local_version >= self._parse_version(message.get("version")):
RedisArchivist().del_message(self.NEW_KEY)
return settings.TA_VERSION

View File

@@ -12,6 +12,7 @@ from datetime import datetime
from urllib.parse import urlparse
import requests
from home.src.ta.settings import EnvironmentSettings
def ignore_filelist(filelist: list[str]) -> list[str]:
@@ -112,13 +113,13 @@ def time_parser(timestamp: str) -> float:
return int(hours) * 60 * 60 + int(minutes) * 60 + float(seconds)
def clear_dl_cache(config: dict) -> int:
def clear_dl_cache(cache_dir: str) -> int:
"""clear leftover files from dl cache"""
print("clear download cache")
cache_dir = os.path.join(config["application"]["cache_dir"], "download")
leftover_files = ignore_filelist(os.listdir(cache_dir))
download_cache_dir = os.path.join(cache_dir, "download")
leftover_files = ignore_filelist(os.listdir(download_cache_dir))
for cached in leftover_files:
to_delete = os.path.join(cache_dir, cached)
to_delete = os.path.join(download_cache_dir, cached)
os.remove(to_delete)
return len(leftover_files)
@@ -178,7 +179,7 @@ def get_duration_str(seconds: int) -> str:
for unit_label, unit_seconds in units:
if seconds >= unit_seconds:
unit_count, seconds = divmod(seconds, unit_seconds)
duration_parts.append(f"{unit_count}{unit_label}")
duration_parts.append(f"{unit_count:02}{unit_label}")
return " ".join(duration_parts)
@@ -203,3 +204,21 @@ def ta_host_parser(ta_host: str) -> tuple[list[str], list[str]]:
csrf_trusted_origins.append(f"{parsed.scheme}://{parsed.hostname}")
return allowed_hosts, csrf_trusted_origins
def get_stylesheets():
"""Get all valid stylesheets from /static/css"""
app_root = EnvironmentSettings.APP_DIR
stylesheets = os.listdir(os.path.join(app_root, "static/css"))
stylesheets.remove("style.css")
stylesheets.sort()
stylesheets = list(filter(lambda x: x.endswith(".css"), stylesheets))
return stylesheets
def check_stylesheet(stylesheet: str):
"""Check if a stylesheet exists. Return dark.css as a fallback"""
if stylesheet in get_stylesheets():
return stylesheet
return "dark.css"

View File

@@ -0,0 +1,95 @@
"""
Functionality:
- read and write application config backed by ES
- encapsulate persistence of application properties
"""
from os import environ
class EnvironmentSettings:
"""
Handle settings for the application that are driven from the environment.
These will not change when the user is using the application.
These settings are only provided only on startup.
"""
HOST_UID: int = int(environ.get("HOST_UID", False))
HOST_GID: int = int(environ.get("HOST_GID", False))
ENABLE_CAST: bool = bool(environ.get("ENABLE_CAST"))
TZ: str = str(environ.get("TZ", "UTC"))
TA_PORT: int = int(environ.get("TA_PORT", False))
TA_UWSGI_PORT: int = int(environ.get("TA_UWSGI_PORT", False))
TA_USERNAME: str = str(environ.get("TA_USERNAME"))
TA_PASSWORD: str = str(environ.get("TA_PASSWORD"))
# Application Paths
MEDIA_DIR: str = str(environ.get("TA_MEDIA_DIR", "/youtube"))
APP_DIR: str = str(environ.get("TA_APP_DIR", "/app"))
CACHE_DIR: str = str(environ.get("TA_CACHE_DIR", "/cache"))
# Redis
REDIS_HOST: str = str(environ.get("REDIS_HOST"))
REDIS_PORT: int = int(environ.get("REDIS_PORT", 6379))
REDIS_NAME_SPACE: str = str(environ.get("REDIS_NAME_SPACE", "ta:"))
# ElasticSearch
ES_URL: str = str(environ.get("ES_URL"))
ES_PASS: str = str(environ.get("ELASTIC_PASSWORD"))
ES_USER: str = str(environ.get("ELASTIC_USER", "elastic"))
ES_SNAPSHOT_DIR: str = str(
environ.get(
"ES_SNAPSHOT_DIR", "/usr/share/elasticsearch/data/snapshot"
)
)
ES_DISABLE_VERIFY_SSL: bool = bool(environ.get("ES_DISABLE_VERIFY_SSL"))
def print_generic(self):
"""print generic env vars"""
print(
f"""
HOST_UID: {self.HOST_UID}
HOST_GID: {self.HOST_GID}
TZ: {self.TZ}
ENABLE_CAST: {self.ENABLE_CAST}
TA_PORT: {self.TA_PORT}
TA_UWSGI_PORT: {self.TA_UWSGI_PORT}
TA_USERNAME: {self.TA_USERNAME}
TA_PASSWORD: *****"""
)
def print_paths(self):
"""debug paths set"""
print(
f"""
MEDIA_DIR: {self.MEDIA_DIR}
APP_DIR: {self.APP_DIR}
CACHE_DIR: {self.CACHE_DIR}"""
)
def print_redis_conf(self):
"""debug redis conf paths"""
print(
f"""
REDIS_HOST: {self.REDIS_HOST}
REDIS_PORT: {self.REDIS_PORT}
REDIS_NAME_SPACE: {self.REDIS_NAME_SPACE}"""
)
def print_es_paths(self):
"""debug es conf"""
print(
f"""
ES_URL: {self.ES_URL}
ES_PASS: *****
ES_USER: {self.ES_USER}
ES_SNAPSHOT_DIR: {self.ES_SNAPSHOT_DIR}
ES_DISABLE_VERIFY_SSL: {self.ES_DISABLE_VERIFY_SSL}"""
)
def print_all(self):
"""print all"""
self.print_generic()
self.print_paths()
self.print_redis_conf()
self.print_es_paths()

View File

@@ -6,20 +6,22 @@ functionality:
"""
import json
import os
import redis
from home.src.ta.settings import EnvironmentSettings
class RedisBase:
"""connection base for redis"""
REDIS_HOST: str = str(os.environ.get("REDIS_HOST"))
REDIS_PORT: int = int(os.environ.get("REDIS_PORT") or 6379)
NAME_SPACE: str = "ta:"
NAME_SPACE: str = EnvironmentSettings.REDIS_NAME_SPACE
def __init__(self):
self.conn = redis.Redis(host=self.REDIS_HOST, port=self.REDIS_PORT)
self.conn = redis.Redis(
host=EnvironmentSettings.REDIS_HOST,
port=EnvironmentSettings.REDIS_PORT,
decode_responses=True,
)
class RedisArchivist(RedisBase):
@@ -81,7 +83,7 @@ class RedisArchivist(RedisBase):
if not reply:
return []
return [i.decode().lstrip(self.NAME_SPACE) for i in reply]
return [i.lstrip(self.NAME_SPACE) for i in reply]
def list_items(self, query: str) -> list:
"""list all matches"""
@@ -98,65 +100,49 @@ class RedisArchivist(RedisBase):
class RedisQueue(RedisBase):
"""dynamically interact with queues in redis"""
"""
dynamically interact with queues in redis using sorted set
- low score number is first in queue
- add new items with high score number
"""
def __init__(self, queue_name: str):
super().__init__()
self.key = f"{self.NAME_SPACE}{queue_name}"
def get_all(self):
def get_all(self) -> list[str]:
"""return all elements in list"""
result = self.conn.execute_command("LRANGE", self.key, 0, -1)
all_elements = [i.decode() for i in result]
return all_elements
result = self.conn.zrange(self.key, 0, -1)
return result
def length(self) -> int:
"""return total elements in list"""
return self.conn.execute_command("LLEN", self.key)
return self.conn.zcard(self.key)
def in_queue(self, element) -> str | bool:
"""check if element is in list"""
result = self.conn.execute_command("LPOS", self.key, element)
result = self.conn.zrank(self.key, element)
if result is not None:
return "in_queue"
return False
def add_list(self, to_add):
def add_list(self, to_add: list) -> None:
"""add list to queue"""
self.conn.execute_command("RPUSH", self.key, *to_add)
def add_priority(self, to_add: str) -> None:
"""add single video to front of queue"""
item: str = json.dumps(to_add)
self.clear_item(item)
self.conn.execute_command("LPUSH", self.key, item)
mapping = {i: "+inf" for i in to_add}
self.conn.zadd(self.key, mapping)
def get_next(self) -> str | bool:
"""return next element in the queue, False if none"""
result = self.conn.execute_command("LPOP", self.key)
"""return next element in the queue, if available"""
result = self.conn.zpopmin(self.key)
if not result:
return False
next_element = result.decode()
return next_element
return result[0][0]
def clear(self) -> None:
"""delete list from redis"""
self.conn.execute_command("DEL", self.key)
def clear_item(self, to_clear: str) -> None:
"""remove single item from list if it's there"""
self.conn.execute_command("LREM", self.key, 0, to_clear)
def trim(self, size: int) -> None:
"""trim the queue based on settings amount"""
self.conn.execute_command("LTRIM", self.key, 0, size)
def has_item(self) -> bool:
"""check if queue as at least one pending item"""
result = self.conn.execute_command("LRANGE", self.key, 0, 0)
return bool(result)
self.conn.delete(self.key)
class TaskRedis(RedisBase):
@@ -169,7 +155,7 @@ class TaskRedis(RedisBase):
def get_all(self) -> list:
"""return all tasks"""
all_keys = self.conn.execute_command("KEYS", f"{self.BASE}*")
return [i.decode().replace(self.BASE, "") for i in all_keys]
return [i.replace(self.BASE, "") for i in all_keys]
def get_single(self, task_id: str) -> dict:
"""return content of single task"""
@@ -177,7 +163,7 @@ class TaskRedis(RedisBase):
if not result:
return {}
return json.loads(result.decode())
return json.loads(result)
def set_key(
self, task_id: str, message: dict, expire: bool | int = False

View File

@@ -7,12 +7,13 @@ Functionality:
from typing import TypedDict
from home.src.es.connect import ElasticWrap
from home.src.ta.helper import get_stylesheets
class UserConfigType(TypedDict, total=False):
"""describes the user configuration"""
colors: str
stylesheet: str
page_size: int
sort_by: str
sort_order: str
@@ -28,15 +29,10 @@ class UserConfigType(TypedDict, total=False):
class UserConfig:
"""Handle settings for an individual user
Create getters and setters for usage in the application.
Although tedious it helps prevents everything caring about how properties
are persisted. Plus it allows us to save anytime any value is set.
"""
"""Handle settings for an individual user"""
_DEFAULT_USER_SETTINGS = UserConfigType(
colors="dark",
stylesheet="dark.css",
page_size=12,
sort_by="published",
sort_order="desc",
@@ -51,9 +47,22 @@ class UserConfig:
sponsorblock_id=None,
)
VALID_STYLESHEETS = get_stylesheets()
VALID_VIEW_STYLE = ["grid", "list"]
VALID_SORT_ORDER = ["asc", "desc"]
VALID_SORT_BY = [
"published",
"downloaded",
"views",
"likes",
"duration",
"filesize",
]
VALID_GRID_ITEMS = range(3, 8)
def __init__(self, user_id: str):
self._user_id: str = user_id
self._config: UserConfigType = self._get_config()
self._config: UserConfigType = self.get_config()
def get_value(self, key: str):
"""Get the given key from the users configuration
@@ -65,15 +74,8 @@ class UserConfig:
return self._config.get(key) or self._DEFAULT_USER_SETTINGS.get(key)
def set_value(self, key: str, value: str | bool | int):
"""Set or replace a configuration value for the user
Throws a KeyError if the requested Key is not a permitted value"""
if not self._user_id:
raise ValueError("Unable to persist config for null user_id")
if key not in self._DEFAULT_USER_SETTINGS:
raise KeyError(f"Unable to persist config for unknown key '{key}'")
"""Set or replace a configuration value for the user"""
self._validate(key, value)
old = self.get_value(key)
self._config[key] = value
@@ -84,9 +86,45 @@ class UserConfig:
if status < 200 or status > 299:
raise ValueError(f"Failed storing user value {status}: {response}")
print(f"User {self._user_id} value '{key}' change: {old} > {value}")
print(f"User {self._user_id} value '{key}' change: {old} -> {value}")
def _get_config(self) -> UserConfigType:
def _validate(self, key, value):
"""validate key and value"""
if not self._user_id:
raise ValueError("Unable to persist config for null user_id")
if key not in self._DEFAULT_USER_SETTINGS:
raise KeyError(
f"Unable to persist config for an unknown key '{key}'"
)
valid_values = {
"stylesheet": self.VALID_STYLESHEETS,
"sort_by": self.VALID_SORT_BY,
"sort_order": self.VALID_SORT_ORDER,
"view_style_home": self.VALID_VIEW_STYLE,
"view_style_channel": self.VALID_VIEW_STYLE,
"view_style_download": self.VALID_VIEW_STYLE,
"view_style_playlist": self.VALID_VIEW_STYLE,
"grid_items": self.VALID_GRID_ITEMS,
"page_size": int,
"hide_watched": bool,
"show_ignored_only": bool,
"show_subed_only": bool,
}
validation_value = valid_values.get(key)
if isinstance(validation_value, (list, range)):
if value not in validation_value:
raise ValueError(f"Invalid value for {key}: {value}")
elif validation_value == int:
if not isinstance(value, int):
raise ValueError(f"Invalid value for {key}: {value}")
elif validation_value == bool:
if not isinstance(value, bool):
raise ValueError(f"Invalid value for {key}: {value}")
def get_config(self) -> UserConfigType:
"""get config from ES or load from the application defaults"""
if not self._user_id:
# this is for a non logged-in user so use all the defaults

View File

@@ -24,13 +24,14 @@ from home.src.index.manual import ImportFolderScanner
from home.src.index.reindex import Reindex, ReindexManual, ReindexPopulate
from home.src.ta.config import AppConfig, ReleaseVersion, ScheduleBuilder
from home.src.ta.notify import Notifications
from home.src.ta.settings import EnvironmentSettings
from home.src.ta.ta_redis import RedisArchivist
from home.src.ta.task_manager import TaskManager
from home.src.ta.urlparser import Parser
CONFIG = AppConfig().config
REDIS_HOST = os.environ.get("REDIS_HOST")
REDIS_PORT = os.environ.get("REDIS_PORT") or 6379
REDIS_HOST = EnvironmentSettings.REDIS_HOST
REDIS_PORT = EnvironmentSettings.REDIS_PORT
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
app = Celery(
@@ -39,9 +40,11 @@ app = Celery(
backend=f"redis://{REDIS_HOST}:{REDIS_PORT}",
result_extended=True,
)
app.config_from_object("django.conf:settings", namespace="ta:")
app.config_from_object(
"django.conf:settings", namespace=EnvironmentSettings.REDIS_NAME_SPACE
)
app.autodiscover_tasks()
app.conf.timezone = os.environ.get("TZ") or "UTC"
app.conf.timezone = EnvironmentSettings.TZ
class BaseTask(Task):
@@ -291,7 +294,7 @@ def run_restore_backup(self, filename):
if manager.is_pending(self):
print(f"[task][{self.name}] restore is already running")
self.send_progress("Restore is already running.")
return
return None
manager.init(self)
self.send_progress(["Reset your Index"])
@@ -299,6 +302,8 @@ def run_restore_backup(self, filename):
ElasticBackup(task=self).restore(filename)
print("index restore finished")
return f"backup restore completed: {filename}"
@shared_task(bind=True, name="rescan_filesystem", base=BaseTask)
def rescan_filesystem(self):

View File

@@ -1,4 +1,5 @@
{% load static %}
{% load auth_extras %}
<!DOCTYPE html>
<html lang="en">
<head>
@@ -22,11 +23,7 @@
{% else %}
<title>TubeArchivist</title>
{% endif %}
{% if colors == "dark" %}
<link rel="stylesheet" href="{% static 'css/dark.css' %}">
{% else %}
<link rel="stylesheet" href="{% static 'css/light.css' %}">
{% endif %}
<link rel="stylesheet" href="{% static 'css/' %}{{ stylesheet }}">
<script type="text/javascript" src="{% static 'script.js' %}"></script>
{% if cast %}
<script type="text/javascript" src="https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1"></script>
@@ -36,16 +33,9 @@
<body>
<div class="main-content">
<div class="boxed-content">
<div class="top-banner">
<a href="{% url 'home' %}">
{% if colors == 'dark' %}
<img src="{% static 'img/banner-tube-archivist-dark.png' %}" alt="tube-archivist-banner">
{% endif %}
{% if colors == 'light' %}
<img src="{% static 'img/banner-tube-archivist-light.png' %}" alt="tube-archivist-banner">
{% endif %}
</a>
</div>
<a href="{% url 'home' %}">
<div class="top-banner"></div>
</a>
<div class="top-nav">
<div class="nav-items">
<a href="{% url 'home' %}">
@@ -57,9 +47,11 @@
<a href="{% url 'playlist' %}">
<div class="nav-item">playlists</div>
</a>
{% if request.user|has_group:"admin" or request.user.is_staff %}
<a href="{% url 'downloads' %}">
<div class="nav-item">downloads</div>
</a>
{% endif %}
</div>
<div class="nav-icons">
<a href="{% url 'search' %}">

View File

@@ -1,14 +1,17 @@
{# Base file for all of the settings pages to ensure a common menu #}
{% extends "home/base.html" %}
{% load static %}
{% load auth_extras %}
{% block content %}
<div class="boxed-content">
<div class="info-box-item child-page-nav">
<a href="{% url 'settings' %}"><h3>Dashboard</h3></a>
<a href="{% url 'settings_user' %}"><h3>User</h3></a>
<a href="{% url 'settings_application' %}"><h3>Application</h3></a>
<a href="{% url 'settings_scheduling' %}"><h3>Scheduling</h3></a>
<a href="{% url 'settings_actions' %}"><h3>Actions</h3></a>
{% if request.user|has_group:"admin" or request.user.is_staff %}
<a href="{% url 'settings_application' %}"><h3>Application</h3></a>
<a href="{% url 'settings_scheduling' %}"><h3>Scheduling</h3></a>
<a href="{% url 'settings_actions' %}"><h3>Actions</h3></a>
{% endif %}
</div>
<div id="notifications" data=""></div>
{% block settings_content %}{% endblock %}

View File

@@ -2,11 +2,13 @@
{% load static %}
{% load humanize %}
{% block content %}
{% load auth_extras %}
<div class="boxed-content">
<div class="title-split">
<div class="title-bar">
<h1>Channels</h1>
</div>
{% if request.user|has_group:"admin" or request.user.is_staff %}
<div class="title-split-form">
<img id="animate-icon" onclick="showForm()" src="{% static 'img/icon-add.svg' %}" alt="add-icon" title="Subscribe to Channels">
<div class="show-form">
@@ -17,6 +19,7 @@
</form>
</div>
</div>
{% endif %}
</div>
<div id="notifications" data="subscription"></div>
<div class="view-controls">

View File

@@ -2,6 +2,8 @@
{% block content %}
{% load static %}
{% load humanize %}
{% load auth_extras %}
<div class="boxed-content">
<div class="channel-banner">
<a href="/channel/{{ channel_info.channel_id }}/"><img src="/cache/channels/{{ channel_info.channel_id }}_banner.jpg" alt="channel_banner"></a>
@@ -19,7 +21,9 @@
{% endif %}
<a href="{% url 'channel_id_about' channel_info.channel_id %}"><h3>About</h3></a>
{% if has_pending %}
<a href="{% url 'downloads' %}?channel={{ channel_info.channel_id }}"><h3>Downloads</h3></a>
{% if request.user|has_group:"admin" or request.user.is_staff %}
<a href="{% url 'downloads' %}?channel={{ channel_info.channel_id }}"><h3>Downloads</h3></a>
{% endif %}
{% endif %}
</div>
<div id="notifications" data="channel reindex"></div>
@@ -38,7 +42,9 @@
<p>Subscribers: {{ channel_info.channel_subs|intcomma }}</p>
{% endif %}
{% if channel_info.channel_subscribed %}
{% if request.user|has_group:"admin" or request.user.is_staff %}
<button class="unsubscribe" type="button" data-type="channel" data-subscribe="" data-id="{{ channel_info.channel_id }}" onclick="subscribeStatus(this)" title="Unsubscribe from {{ channel_info.channel_name }}">Unsubscribe</button>
{% endif %}
{% else %}
<button type="button" data-type="channel" data-subscribe="true" data-id="{{ channel_info.channel_id }}" onclick="subscribeStatus(this)" title="Subscribe to {{ channel_info.channel_name }}">Subscribe</button>
{% endif %}
@@ -71,13 +77,15 @@
<div class="sort">
<div id="hidden-form">
<span>Sort by:</span>
<select name="sort" id="sort" onchange="sortChange(this.value)">
<select name="sort_by" id="sort" onchange="sortChange(this)">
<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>
<option value="duration" {% if sort_by == "duration" %}selected{% endif %}>duration</option>
<option value="filesize" {% if sort_by == "filesize" %}selected{% endif %}>file size</option>
</select>
<select name="sord-order" id="sort-order" onchange="sortChange(this.value)">
<select name="sort_order" id="sort-order" onchange="sortChange(this)">
<option value="asc" {% if sort_order == "asc" %}selected{% endif %}>asc</option>
<option value="desc" {% if sort_order == "desc" %}selected{% endif %}>desc</option>
</select>

View File

@@ -2,6 +2,7 @@
{% block content %}
{% load static %}
{% load humanize %}
{% load auth_extras %}
<div class="boxed-content">
<div class="channel-banner">
<a href="{% url 'channel_id' channel_info.channel_id %}"><img src="{{ channel_info.channel_banner_url }}" alt="channel_banner"></a>
@@ -19,7 +20,9 @@
{% endif %}
<a href="{% url 'channel_id_about' channel_info.channel_id %}"><h3>About</h3></a>
{% if has_pending %}
<a href="{% url 'downloads' %}?channel={{ channel_info.channel_id }}"><h3>Downloads</h3></a>
{% if request.user|has_group:"admin" or request.user.is_staff %}
<a href="{% url 'downloads' %}?channel={{ channel_info.channel_id }}"><h3>Downloads</h3></a>
{% endif %}
{% endif %}
</div>
<div id="notifications" data="channel reindex"></div>
@@ -56,19 +59,21 @@
{% elif channel_info.channel_views > 0 %}
<p>Channel views: {{ channel_info.channel_views|intcomma }}</p>
{% endif %}
<div class="button-box">
<button onclick="deleteConfirm()" id="delete-item">Delete Channel</button>
<div class="delete-confirm" id="delete-button">
<span>Delete {{ channel_info.channel_name }} including all videos? </span><button class="danger-button" onclick="deleteChannel(this)" data-id="{{ channel_info.channel_id }}">Delete</button> <button onclick="cancelDelete()">Cancel</button>
</div>
</div>
{% if reindex %}
<p>Reindex scheduled</p>
{% else %}
<div id="reindex-button" class="button-box">
<button data-id="{{ channel_info.channel_id }}" data-type="channel" onclick="reindex(this)" title="Reindex Channel {{ channel_info.channel_name }}">Reindex</button>
<button data-id="{{ channel_info.channel_id }}" data-type="channel" data-extract-videos="true" onclick="reindex(this)" title="Reindex Videos of {{ channel_info.channel_name }}">Reindex Videos</button>
{% if request.user|has_group:"admin" or request.user.is_staff %}
<div class="button-box">
<button onclick="deleteConfirm()" id="delete-item">Delete Channel</button>
<div class="delete-confirm" id="delete-button">
<span>Delete {{ channel_info.channel_name }} including all videos? </span><button class="danger-button" onclick="deleteChannel(this)" data-id="{{ channel_info.channel_id }}">Delete</button> <button onclick="cancelDelete()">Cancel</button>
</div>
</div>
{% if reindex %}
<p>Reindex scheduled</p>
{% else %}
<div id="reindex-button" class="button-box">
<button data-id="{{ channel_info.channel_id }}" data-type="channel" onclick="reindex(this)" title="Reindex Channel {{ channel_info.channel_name }}">Reindex</button>
<button data-id="{{ channel_info.channel_id }}" data-type="channel" data-extract-videos="true" onclick="reindex(this)" title="Reindex Videos of {{ channel_info.channel_name }}">Reindex Videos</button>
</div>
{% endif %}
{% endif %}
</div>
</div>
@@ -90,53 +95,55 @@
</div>
</div>
{% endif %}
<div id="overwrite-form" class="info-box">
<div class="info-box-item">
<h2>Customize {{ channel_info.channel_name }}</h2>
<form class="overwrite-form" action="/channel/{{ channel_info.channel_id }}/about/" method="POST">
{% csrf_token %}
<div class="overwrite-form-item">
<p>Download format: <span class="settings-current">
{% if channel_info.channel_overwrites.download_format %}
{{ channel_info.channel_overwrites.download_format }}
{% else %}
False
{% endif %}</span></p>
{{ channel_overwrite_form.download_format }}<br>
</div>
<div class="overwrite-form-item">
<p>Auto delete watched videos after x days: <span class="settings-current">
{% if channel_info.channel_overwrites.autodelete_days %}
{{ channel_info.channel_overwrites.autodelete_days }}
{% else %}
False
{% endif %}</span></p>
{{ channel_overwrite_form.autodelete_days }}<br>
</div>
<div class="overwrite-form-item">
<p>Index playlists: <span class="settings-current">
{% if channel_info.channel_overwrites.index_playlists %}
{{ channel_info.channel_overwrites.index_playlists }}
{% else %}
False
{% endif %}</span></p>
{{ channel_overwrite_form.index_playlists }}<br>
</div>
<div class="overwrite-form-item">
<p>Enable <a href="https://sponsor.ajay.app/" target="_blank">SponsorBlock</a>: <span class="settings-current">
{% if channel_info.channel_overwrites.integrate_sponsorblock %}
{{ channel_info.channel_overwrites.integrate_sponsorblock }}
{% elif channel_info.channel_overwrites.integrate_sponsorblock == False %}
Disabled
{% else %}
False
{% endif %}</span></p>
{{ channel_overwrite_form.integrate_sponsorblock }}<br>
</div>
<button type="submit">Save Channel Overwrites</button>
</form>
{% if request.user|has_group:"admin" or request.user.is_staff %}
<div id="overwrite-form" class="info-box">
<div class="info-box-item">
<h2>Customize {{ channel_info.channel_name }}</h2>
<form class="overwrite-form" action="/channel/{{ channel_info.channel_id }}/about/" method="POST">
{% csrf_token %}
<div class="overwrite-form-item">
<p>Download format: <span class="settings-current">
{% if channel_info.channel_overwrites.download_format %}
{{ channel_info.channel_overwrites.download_format }}
{% else %}
False
{% endif %}</span></p>
{{ channel_overwrite_form.download_format }}<br>
</div>
<div class="overwrite-form-item">
<p>Auto delete watched videos after x days: <span class="settings-current">
{% if channel_info.channel_overwrites.autodelete_days %}
{{ channel_info.channel_overwrites.autodelete_days }}
{% else %}
False
{% endif %}</span></p>
{{ channel_overwrite_form.autodelete_days }}<br>
</div>
<div class="overwrite-form-item">
<p>Index playlists: <span class="settings-current">
{% if channel_info.channel_overwrites.index_playlists %}
{{ channel_info.channel_overwrites.index_playlists }}
{% else %}
False
{% endif %}</span></p>
{{ channel_overwrite_form.index_playlists }}<br>
</div>
<div class="overwrite-form-item">
<p>Enable <a href="https://sponsor.ajay.app/" target="_blank">SponsorBlock</a>: <span class="settings-current">
{% if channel_info.channel_overwrites.integrate_sponsorblock %}
{{ channel_info.channel_overwrites.integrate_sponsorblock }}
{% elif channel_info.channel_overwrites.integrate_sponsorblock == False %}
Disabled
{% else %}
False
{% endif %}</span></p>
{{ channel_overwrite_form.integrate_sponsorblock }}<br>
</div>
<button type="submit">Save Channel Overwrites</button>
</form>
</div>
</div>
</div>
{% endif %}
</div>
<script type="text/javascript" src="{% static 'progress.js' %}"></script>
{% endblock content %}

View File

@@ -2,6 +2,7 @@
{% block content %}
{% load static %}
{% load humanize %}
{% load auth_extras %}
<div class="boxed-content">
<div class="channel-banner">
<a href="{% url 'channel_id' channel_info.channel_id %}"><img src="{{ channel_info.channel_banner_url }}" alt="channel_banner"></a>
@@ -19,7 +20,9 @@
{% endif %}
<a href="{% url 'channel_id_about' channel_info.channel_id %}"><h3>About</h3></a>
{% if has_pending %}
<a href="{% url 'downloads' %}?channel={{ channel_info.channel_id }}"><h3>Downloads</h3></a>
{% if request.user|has_group:"admin" or request.user.is_staff %}
<a href="{% url 'downloads' %}?channel={{ channel_info.channel_id }}"><h3>Downloads</h3></a>
{% endif %}
{% endif %}
</div>
<div id="notifications" data="channel reindex"></div>
@@ -53,7 +56,7 @@
<a href="{% url 'channel_id' playlist.playlist_channel_id %}"><h3>{{ playlist.playlist_channel }}</h3></a>
<a href="{% url 'playlist_id' playlist.playlist_id %}"><h2>{{ playlist.playlist_name }}</h2></a>
<p>Last refreshed: {{ playlist.playlist_last_refresh }}</p>
{% if playlist.playlist_subscribed %}
{% if playlist.playlist_subscribed and request.user|has_group:"admin" or request.user.is_staff %}
<button class="unsubscribe" type="button" data-type="playlist" data-subscribe="" data-id="{{ playlist.playlist_id }}" onclick="subscribeStatus(this)" title="Unsubscribe from {{ playlist.playlist_name }}">Unsubscribe</button>
{% else %}
<button type="button" data-type="playlist" data-subscribe="true" data-id="{{ playlist.playlist_id }}" onclick="subscribeStatus(this)" title="Subscribe to {{ playlist.playlist_name }}">Subscribe</button>

View File

@@ -60,13 +60,15 @@
<div class="sort">
<div id="hidden-form">
<span>Sort by:</span>
<select name="sort" id="sort" onchange="sortChange(this.value)">
<select name="sort_by" id="sort" onchange="sortChange(this)">
<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>
<option value="duration" {% if sort_by == "duration" %}selected{% endif %}>duration</option>
<option value="filesize" {% if sort_by == "filesize" %}selected{% endif %}>file size</option>
</select>
<select name="sord-order" id="sort-order" onchange="sortChange(this.value)">
<select name="sort_order" id="sort-order" onchange="sortChange(this)">
<option value="asc" {% if sort_order == "asc" %}selected{% endif %}>asc</option>
<option value="desc" {% if sort_order == "desc" %}selected{% endif %}>desc</option>
</select>

View File

@@ -18,20 +18,11 @@
<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 %}
<link rel="stylesheet" href="{% static 'css/light.css' %}">
{% endif %}
<link rel="stylesheet" href="{% static 'css/' %}{{ stylesheet }}">
</head>
<body>
<div class="boxed-content login-page">
{% if colors == 'dark' %}
<img src="{% static 'img/logo-tube-archivist-dark.png' %}" alt="tube-archivist-logo">
{% endif %}
{% if colors == 'light' %}
<img src="{% static 'img/logo-tube-archivist-light.png' %}" alt="tube-archivist-banner">
{% endif %}
<img alt="tube-archivist-logo">
<h1>Tube Archivist</h1>
<h2>Your Self Hosted YouTube Media Server</h2>
{% if form_error %}

View File

@@ -1,11 +1,15 @@
{% extends "home/base.html" %}
{% load static %}
{% block content %}
{% load auth_extras %}
<div class="boxed-content">
<div class="title-split">
<div class="title-bar">
<h1>Playlists</h1>
</div>
{% if request.user|has_group:"admin" or request.user.is_staff %}
<div class="title-split-form">
<img id="animate-icon" onclick="showForm()" src="{% static 'img/icon-add.svg' %}" alt="add-icon" title="Subscribe to Playlists">
<div class="show-form">
@@ -16,6 +20,8 @@
</form>
</div>
</div>
{% endif %}
</div>
<div id="notifications" data="subscription"></div>
<div class="view-controls">

View File

@@ -2,6 +2,8 @@
{% load static %}
{% load humanize %}
{% block content %}
{% load auth_extras %}
<div class="boxed-content">
<div class="title-bar">
<h1>{{ playlist_info.playlist_name }}</h1>
@@ -27,7 +29,9 @@
<p>Last refreshed: {{ playlist_info.playlist_last_refresh }}</p>
<p>Playlist:
{% if playlist_info.playlist_subscribed %}
{% if request.user|has_group:"admin" or request.user.is_staff %}
<button class="unsubscribe" type="button" data-type="playlist" data-subscribe="" data-id="{{ playlist_info.playlist_id }}" onclick="subscribeStatus(this)" title="Unsubscribe from {{ playlist_info.playlist_name }}">Unsubscribe</button>
{% endif %}
{% else %}
<button type="button" data-type="playlist" data-subscribe="true" data-id="{{ playlist_info.playlist_id }}" onclick="subscribeStatus(this)" title="Subscribe to {{ playlist_info.playlist_name }}">Subscribe</button>
{% endif %}

View File

@@ -5,14 +5,26 @@
<h1>Your Archive</h1>
</div>
<div class="settings-item">
<h2>Main overview</h2>
<div id="primaryBox" class="info-box info-box-4">
<h2>Overview</h2>
<div id="activeBox" class="info-box info-box-3">
<p id="loading">Loading...</p>
</div>
</div>
<div class="settings-item">
<h2>Video Type</h2>
<div id="videoTypeBox" class="info-box info-box-3">
<p id="loading">Loading...</p>
</div>
</div>
<div class="settings-item">
<h2>Application</h2>
<div id="secondaryBox" class="info-box info-box-3">
<p id="loading">Loading...</p>
</div>
</div>
<div class="settings-item">
<h2>Watch Progress</h2>
<div id="watchBox" class="info-box info-box-3">
<div id="watchBox" class="info-box info-box-2">
<p id="loading">Loading...</p>
</div>
</div>

View File

@@ -51,7 +51,7 @@
</div>
</div>
<div class="settings-group">
<h2>Start download</h2>
<h2>Start Download</h2>
<div class="settings-item">
<p>Current Download schedule: <span class="settings-current">
{% if config.scheduler.download_pending %}
@@ -112,7 +112,7 @@
</div>
</div>
<div class="settings-group">
<h2>Thumbnail check</h2>
<h2>Thumbnail Check</h2>
<div class="settings-item">
<p>Current thumbnail check schedule: <span class="settings-current">
{% if config.scheduler.thumbnail_check %}

View File

@@ -7,11 +7,11 @@
<form action="{% url 'settings_user' %}" method="POST" name="user-update">
{% csrf_token %}
<div class="settings-group">
<h2>Color scheme</h2>
<h2>Stylesheet</h2>
<div class="settings-item">
<p>Current color scheme: <span class="settings-current">{{ colors }}</span></p>
<i>Select your preferred color scheme between dark and light mode.</i><br>
{{ user_form.colors }}
<p>Current stylesheet: <span class="settings-current">{{ stylesheet }}</span></p>
<i>Select your preferred stylesheet.</i><br>
{{ user_form.stylesheet }}
</div>
</div>
<div class="settings-group">

View File

@@ -2,6 +2,7 @@
{% block content %}
{% load static %}
{% load humanize %}
{% load auth_extras %}
<div id="player" class="player-wrapper">
<div class="video-main">
<div class="video-modal"><span class="video-modal-text"></span></div>
@@ -81,15 +82,19 @@
{% if reindex %}
<p>Reindex scheduled</p>
{% else %}
{% if request.user|has_group:"admin" or request.user.is_staff %}
<div id="reindex-button" class="button-box">
<button data-id="{{ video.youtube_id }}" data-type="video" onclick="reindex(this)" title="Reindex {{ video.title }}">Reindex</button>
</div>
{% endif %}
{% endif %}
<a download="" href="/media/{{ video.media_url }}"><button id="download-item">Download File</button></a>
<a download="" href="{{ video.media_url }}"><button id="download-item">Download File</button></a>
{% if request.user|has_group:"admin" or request.user.is_staff %}
<button onclick="deleteConfirm()" id="delete-item">Delete Video</button>
<div class="delete-confirm" id="delete-button">
<span>Are you sure? </span><button class="danger-button" onclick="deleteVideo(this)" data-id="{{ video.youtube_id }}" data-redirect = "{{ video.channel.channel_id }}">Delete</button> <button onclick="cancelDelete()">Cancel</button>
</div>
{% endif %}
</div>
</div>
<div class="info-box-item">

View File

@@ -0,0 +1,8 @@
from django import template
register = template.Library()
@register.filter(name="has_group")
def has_group(user, group_name):
return user.groups.filter(name=group_name).exists()

View File

@@ -58,7 +58,6 @@ urlpatterns = [
login_required(views.SettingsActionsView.as_view()),
name="settings_actions",
),
path("process/", login_required(views.process), name="process"),
path(
"channel/",
login_required(views.ChannelView.as_view()),

View File

@@ -4,23 +4,24 @@ Functionality:
- holds base classes to inherit from
"""
import enum
import json
import urllib.parse
from time import sleep
from api.src.search_processor import SearchProcess, process_aggs
from api.views import check_admin
from django.conf import settings
from django.contrib.auth import login
from django.contrib.auth.decorators import user_passes_test
from django.contrib.auth.forms import AuthenticationForm
from django.http import Http404, JsonResponse
from django.http import Http404
from django.shortcuts import redirect, render
from django.utils.decorators import method_decorator
from django.views import View
from home.src.download.queue import PendingInteract
from home.src.download.yt_dlp_base import CookieHandler
from home.src.es.backup import ElasticBackup
from home.src.es.connect import ElasticWrap
from home.src.es.snapshot import ElasticSnapshot
from home.src.frontend.api_calls import PostData
from home.src.frontend.forms import (
AddToQueueForm,
ApplicationSettingsForm,
@@ -38,7 +39,8 @@ from home.src.index.playlist import YoutubePlaylist
from home.src.index.reindex import ReindexProgress
from home.src.index.video_constants import VideoTypeEnum
from home.src.ta.config import AppConfig, ReleaseVersion, ScheduleBuilder
from home.src.ta.helper import time_parser
from home.src.ta.helper import check_stylesheet, time_parser
from home.src.ta.settings import EnvironmentSettings
from home.src.ta.ta_redis import RedisArchivist
from home.src.ta.users import UserConfig
from home.tasks import index_channel_playlists, subscribe_to
@@ -53,7 +55,6 @@ class ArchivistViewConfig(View):
self.view_origin = view_origin
self.user_id = False
self.user_conf: UserConfig = False
self.default_conf = False
self.context = False
def get_all_view_styles(self):
@@ -70,11 +71,12 @@ class ArchivistViewConfig(View):
"""build default context for every view"""
self.user_id = user_id
self.user_conf = UserConfig(self.user_id)
self.default_conf = AppConfig().config
self.context = {
"colors": self.user_conf.get_value("colors"),
"cast": self.default_conf["application"]["enable_cast"],
"stylesheet": check_stylesheet(
self.user_conf.get_value("stylesheet")
),
"cast": EnvironmentSettings.ENABLE_CAST,
"sort_by": self.user_conf.get_value("sort_by"),
"sort_order": self.user_conf.get_value("sort_order"),
"view_style": self.user_conf.get_value(
@@ -109,6 +111,8 @@ class ArchivistResultsView(ArchivistViewConfig):
"likes": "stats.like_count",
"downloaded": "date_downloaded",
"published": "published",
"duration": "player.duration",
"filesize": "media_size",
}
sort_by = sort_by_map[self.context["sort_by"]]
@@ -220,7 +224,9 @@ class MinView(View):
def get_min_context(request):
"""build minimal vars for context"""
return {
"colors": UserConfig(request.user.id).get_value("colors"),
"stylesheet": check_stylesheet(
UserConfig(request.user.id).get_value("stylesheet")
),
"version": settings.TA_VERSION,
"ta_update": ReleaseVersion().get_update(),
}
@@ -317,6 +323,7 @@ class AboutView(MinView):
return render(request, "home/about.html", context)
@method_decorator(user_passes_test(check_admin), name="dispatch")
class DownloadView(ArchivistResultsView):
"""resolves to /download/
handle the download queue
@@ -597,6 +604,7 @@ class ChannelIdAboutView(ChannelIdBaseView):
return render(request, "home/channel_id_about.html", self.context)
@method_decorator(user_passes_test(check_admin), name="dispatch")
@staticmethod
def post(request, channel_id):
"""handle post request"""
@@ -681,6 +689,7 @@ class ChannelView(ArchivistResultsView):
"term": {"channel_subscribed": {"value": True}}
}
@method_decorator(user_passes_test(check_admin), name="dispatch")
@staticmethod
def post(request):
"""handle http post requests"""
@@ -824,6 +833,7 @@ class PlaylistView(ArchivistResultsView):
}
}
@method_decorator(user_passes_test(check_admin), name="dispatch")
@staticmethod
def post(request):
"""handle post from search form"""
@@ -870,7 +880,7 @@ class VideoView(MinView):
"video": video_data,
"playlist_nav": playlist_nav,
"title": video_data.get("title"),
"cast": config_handler.config["application"]["enable_cast"],
"cast": EnvironmentSettings.ENABLE_CAST,
"config": config_handler.config,
"position": time_parser(request.GET.get("t")),
"reindex": reindex.get("state"),
@@ -973,9 +983,9 @@ class SettingsUserView(MinView):
config_handler = UserConfig(request.user.id)
if user_form.is_valid():
user_form_post = user_form.cleaned_data
if user_form_post.get("colors"):
if user_form_post.get("stylesheet"):
config_handler.set_value(
"colors", user_form_post.get("colors")
"stylesheet", user_form_post.get("stylesheet")
)
if user_form_post.get("page_size"):
config_handler.set_value(
@@ -986,6 +996,7 @@ class SettingsUserView(MinView):
return redirect("settings_user", permanent=True)
@method_decorator(user_passes_test(check_admin), name="dispatch")
class SettingsApplicationView(MinView):
"""resolves to /settings/application/
handle the settings sub-page for application configuration,
@@ -1075,6 +1086,7 @@ class SettingsApplicationView(MinView):
RedisArchivist().set_message(key, message=message, expire=True)
@method_decorator(user_passes_test(check_admin), name="dispatch")
class SettingsSchedulingView(MinView):
"""resolves to /settings/scheduling/
handle the settings sub-page for scheduling settings,
@@ -1108,6 +1120,7 @@ class SettingsSchedulingView(MinView):
return redirect("settings_scheduling", permanent=True)
@method_decorator(user_passes_test(check_admin), name="dispatch")
class SettingsActionsView(MinView):
"""resolves to /settings/actions/
handle the settings actions sub-page
@@ -1124,16 +1137,3 @@ class SettingsActionsView(MinView):
)
return render(request, "home/settings_actions.html", context)
def process(request):
"""handle all the buttons calls via POST ajax"""
if request.method == "POST":
current_user = request.user.id
post_dict = json.loads(request.body.decode())
post_handler = PostData(post_dict, current_user)
if post_handler.to_exec:
task_result = post_handler.run_task()
return JsonResponse(task_result)
return JsonResponse({"success": False})

View File

@@ -1,13 +1,13 @@
apprise==1.5.0
celery==5.3.4
Django==4.2.6
apprise==1.6.0
celery==5.3.6
Django==4.2.7
django-auth-ldap==4.6.0
django-cors-headers==4.2.0
django-cors-headers==4.3.1
djangorestframework==3.14.0
Pillow==10.0.1
redis==5.0.1
Pillow==10.1.0
redis==5.0.0
requests==2.31.0
ryd-client==0.0.6
uWSGI==2.0.22
whitenoise==6.5.0
yt-dlp==2023.10.7
uWSGI==2.0.23
whitenoise==6.6.0
yt-dlp @ git+https://github.com/yt-dlp/yt-dlp@6b5d93b0b0240e287389d1d43b2d5293e18aa4cc

View File

@@ -9,4 +9,6 @@
--accent-font-light: #97d4c8;
--img-filter: invert(50%) sepia(9%) saturate(2940%) hue-rotate(122deg) brightness(94%) contrast(90%);
--img-filter-error: invert(16%) sepia(60%) saturate(3717%) hue-rotate(349deg) brightness(86%) contrast(120%);
--banner: url("../img/banner-tube-archivist-dark.png");
--logo: url("../img/logo-tube-archivist-dark.png");
}

View File

@@ -9,4 +9,6 @@
--accent-font-light: #35b399;
--img-filter: invert(50%) sepia(9%) saturate(2940%) hue-rotate(122deg) brightness(94%) contrast(90%);
--img-filter-error: invert(16%) sepia(60%) saturate(3717%) hue-rotate(349deg) brightness(86%) contrast(120%);
--banner: url("../img/banner-tube-archivist-light.png");
--logo: url("../img/logo-tube-archivist-light.png");
}

View File

@@ -0,0 +1,67 @@
:root {
--main-bg: #000000;
--highlight-bg: #080808;
--highlight-error: #880000;
--highlight-error-light: #aa0000;
--highlight-bg-transparent: #0c0c0caf;
--main-font: #00aa00;
--accent-font-dark: #007700;
--accent-font-light: #00aa00;
--img-filter: brightness(0) saturate(100%) invert(45%) sepia(100%) saturate(3710%) hue-rotate(96deg) brightness(100%) contrast(102%);
--img-filter-error: invert(16%) sepia(60%) saturate(3717%) hue-rotate(349deg) brightness(86%) contrast(120%);
--banner: url("../img/banner-tube-archivist-dark.png");
--logo: url("../img/logo-tube-archivist-dark.png");
--outline: 1px solid green;
--filter: hue-rotate(310deg);
}
.settings-group {
outline: var(--outline);
}
.info-box-item {
outline: var(--outline);
}
.footer {
outline: var(--outline);
}
.top-banner img {
filter: var(--filter);
}
.icon-text {
outline: var(--outline);
}
.video-item {
outline: var(--outline);
}
.channel-banner {
outline: var(--outline);
}
.description-box {
outline: var(--outline);
}
.video-player {
outline: var(--outline);
}
#notification {
outline: var(--outline);
}
textarea {
background-color: var(--highlight-bg);
outline: var(--outline);
color: var(--main-font);
}
input {
background-color: var(--highlight-bg);
color: var(--main-font);
}

View File

@@ -0,0 +1,14 @@
:root {
--main-bg: #000000;
--highlight-bg: #0c0c0c;
--highlight-error: #220000;
--highlight-error-light: #330000;
--highlight-bg-transparent: #0c0c0caf;
--main-font: #888888;
--accent-font-dark: #555555;
--accent-font-light: #999999;
--img-filter: invert(50%) sepia(9%) saturate(2940%) hue-rotate(122deg) brightness(94%) contrast(90%);
--img-filter-error: invert(16%) sepia(60%) saturate(3717%) hue-rotate(349deg) brightness(86%) contrast(120%);
--banner: url("../img/banner-tube-archivist-dark.png");
--logo: url("../img/logo-tube-archivist-dark.png");
}

View File

@@ -159,18 +159,19 @@ button:hover {
}
.top-banner {
text-align: center;
}
.top-banner img {
width: 100%;
max-width: 700px;
background-image: var(--banner);
background-repeat: no-repeat;
background-size: contain;
height: 10vh;
min-height: 80px;
max-height: 120px;
background-position: center center;
}
.footer {
margin: 0;
padding: 20px 0;
background-color: var(--accent-font-dark);
background-color: var(--highlight-bg);
grid-row-start: 2;
grid-row-end: 3;
}
@@ -179,6 +180,10 @@ button:hover {
text-decoration: underline;
}
.footer .boxed-content {
text-align: center;
}
/* toggle on-off */
.toggle {
display: flex;
@@ -725,6 +730,7 @@ video:-webkit-full-screen {
max-width: 200px;
max-height: 200px;
margin-bottom: 40px;
content: var(--logo);
}
.login-page form {

View File

@@ -1,71 +1,13 @@
<?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="Icon_add.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.85859018"
inkscape:cx="-97.380081"
inkscape:cy="261.09215"
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"
showguides="true"
inkscape:guide-bbox="true">
<sodipodi:guide
position="-221.87586,143.2945"
orientation="1,0"
id="guide1072"
inkscape:locked="false" />
</sodipodi:namedview>
<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)">
<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 58.600542,170.62113 c -1.62283,1.61686 -2.626579,3.8573 -2.631447,6.33943 l -0.08037,43.5977 -43.597706,-0.0803 c -4.9648228,-0.009 -8.9691711,3.98046 -8.9784054,8.94536 l -0.00482,2.62846 c -0.00925,4.9649 3.9804459,8.96933 8.9452674,8.97832 l 43.597694,0.0805 -0.08027,43.59778 c -0.0093,4.96488 3.980368,8.96922 8.945263,8.9783 l 2.628471,0.005 c 4.964897,0.009 8.969245,-3.98054 8.978406,-8.94536 l 0.08035,-43.59771 43.597715,0.0803 c 4.96484,0.009 8.96917,-3.98046 8.9784,-8.94534 l 0.005,-2.62847 c 0.009,-4.96489 -3.98037,-8.96923 -8.94525,-8.97831 l -43.597784,-0.0805 0.08034,-43.59771 c 0.0093,-4.96481 -3.980379,-8.96923 -8.945267,-8.97831 l -2.628469,-0.005 c -2.482483,-0.005 -4.724106,0.98906 -6.346936,2.60592 z"
id="rect1073"
inkscape:connector-curvature="0" />
</g>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.5.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="svg1303" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:svg="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 500 500"
style="enable-background:new 0 0 500 500;" xml:space="preserve">
<sodipodi:namedview bordercolor="#666666" borderopacity="1.0" id="base" inkscape:current-layer="layer1" inkscape:cx="-97.380081" inkscape:cy="261.09215" inkscape:document-units="mm" inkscape:guide-bbox="true" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:window-height="1017" inkscape:window-maximized="1" inkscape:window-width="1920" inkscape:window-x="-8" inkscape:window-y="-8" inkscape:zoom="0.85859018" pagecolor="#ffffff" showgrid="false" showguides="true" units="px">
<sodipodi:guide id="guide1072" inkscape:locked="false" orientation="1,0" position="-221.87586,143.2945"></sodipodi:guide>
</sodipodi:namedview>
<path d="M431.9,194.4H305.6V68.1c0-30.6-25-55.6-55.6-55.6h0c-30.6,0-55.6,25-55.6,55.6v126.3H68.1c-30.6,0-55.6,25-55.6,55.6v0
c0,30.6,25,55.6,55.6,55.6h126.3v126.3c0,30.6,25,55.6,55.6,55.6h0c30.6,0,55.6-25,55.6-55.6V305.6h126.3c30.6,0,55.6-25,55.6-55.6
v0C487.5,219.4,462.5,194.4,431.9,194.4z"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,63 +1,10 @@
<?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_close.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.4291625"
inkscape:cx="122.66624"
inkscape:cy="202.38142"
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)">
<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 115.5244,167.09017 c -3.04348,0 -6.08905,1.16742 -8.42111,3.49893 L 66.146003,211.54641 25.18874,170.5891 c -4.664143,-4.66413 -12.173961,-4.66413 -16.8382429,0 l -2.4692132,2.46935 c -4.664282,4.66413 -4.664282,12.17408 0,16.83807 l 40.9572491,40.9573 -40.9572491,40.95745 c -4.664282,4.66412 -4.664282,12.17393 0,16.83806 l 2.4692132,2.46935 c 4.6642819,4.66413 12.1740999,4.66413 16.8382429,0 l 40.957263,-40.9573 40.957287,40.9573 c 4.66413,4.66413 12.17393,4.66413 16.8382,0 l 2.46921,-2.46935 c 4.66427,-4.66413 4.66427,-12.17394 0,-16.83806 L 85.453463,230.85382 126.4107,189.89652 c 4.66427,-4.66399 4.66427,-12.17394 0,-16.83807 l -2.46921,-2.46935 c -2.33221,-2.33206 -5.37361,-3.49893 -8.41709,-3.49893 z"
id="rect1073"
inkscape:connector-curvature="0" />
</g>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.5.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="svg1303" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:svg="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 500 500"
style="enable-background:new 0 0 500 500;" xml:space="preserve">
<path d="M473.5,392.9L329.1,250.5l144.3-145.6c21.6-21.6,21.6-57,0-78.6l0,0c-21.6-21.6-57-21.6-78.6,0L250.5,171.9L106.3,26.2
c-21.6-21.6-57-21.6-78.6,0l0,0C6,47.9,6,83.2,27.6,104.9l144.2,145.6L26.4,393.9c-21.6,21.6-21.6,57,0,78.6l0,0
c21.6,21.6,57,21.6,78.6,0l145.4-143.4l144.3,142.4c21.6,21.6,57,21.6,78.6,0l0,0C495.1,449.9,495.1,414.6,473.5,392.9z"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1,63 +1,14 @@
<?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="Download.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.71458126"
inkscape:cx="-376.84122"
inkscape:cy="260.65826"
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></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Ebene 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-164.70764)">
<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 249.89062 39.6875 L 249.88477 39.693359 C 229.11581 39.693359 212.39453 59.135896 212.39453 83.289062 L 212.39453 289.66016 L 159.20312 236.46875 L 127.63867 268.05469 L 218.74805 359.14258 L 218.73242 359.1582 L 249.93555 390.33789 L 281.52148 358.77344 L 372.36133 267.91211 L 341.1582 236.73047 L 287.37891 290.50977 L 287.37891 83.283203 C 287.37891 59.130339 270.65971 39.6875 249.89062 39.6875 z M 50.566406 407.88867 C 44.284822 407.88867 39.228516 412.94498 39.228516 419.22656 L 39.228516 444.62695 C 39.228516 450.90854 44.284822 455.96484 50.566406 455.96484 L 449.43359 455.96484 C 455.71518 455.96484 460.77148 450.90854 460.77148 444.62695 L 460.77148 419.22656 C 460.77148 412.94498 455.71518 407.88867 449.43359 407.88867 L 50.566406 407.88867 z "
transform="matrix(0.26458394,0,0,0.26458394,0,164.70749)"
id="rect1208" />
</g>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.5.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="svg1303" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:svg="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 500 500"
style="enable-background:new 0 0 500 500;" xml:space="preserve">
<g>
<path d="M231.3,382c4.8,5.3,11.6,8.3,18.7,8.3c7.1,0,13.9-3,18.7-8.3l112.1-124.2c6.7-7.4,8.4-18,4.3-27.1
c-4-9.1-13.1-14.9-23-14.9h-52.9V64.9c0-13.9-11.3-25.2-25.2-25.2h-68c-13.9,0-25.2,11.3-25.2,25.2v150.9h-52.9
c-10,0-19,5.9-23,14.9c-4,9.1-2.3,19.7,4.3,27.1L231.3,382z"/>
<path d="M436.1,408.8H63.9c-13.6,0-24.7,11.1-24.7,24.7c0,13.6,11.1,24.7,24.7,24.7h372.2c13.6,0,24.7-11.1,24.7-24.7
C460.8,419.9,449.7,408.8,436.1,408.8z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,75 +1,19 @@
<?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="210mm"
height="210mm"
viewBox="0 0 210 210"
version="1.1"
id="svg1566"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="Icons_exit 05.svg">
<defs
id="defs1560" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.35355339"
inkscape:cx="963.7258"
inkscape:cy="291.01609"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1009"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1" />
<metadata
id="metadata1563">
<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,-87)">
<path
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.35654187;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 106.49932,87.901069 C 49.504302,87.900974 3.3006913,134.10459 3.3007713,191.0996 c 0,0.30098 0.003,0.60131 0.005,0.90167 v 0 c -0.003,0.29952 -0.006,0.59901 -0.006,0.89912 -8e-5,56.99502 46.2035307,103.19865 103.1985287,103.19854 23.01714,-0.0773 45.34783,-7.84709 63.44155,-22.07425 0,0 9.01874,-8.71006 2.40579,-16.41737 -6.61297,-7.70731 -19.11222,0.3185 -19.11222,0.3185 -13.60985,9.81394 -29.95596,15.11012 -46.73512,15.14236 -44.275428,0 -80.167758,-35.89234 -80.167758,-80.16778 0,-0.30097 0.003,-0.60148 0.006,-0.90166 h -5.2e-4 c -0.003,-0.29934 -0.006,-0.59901 -0.006,-0.89913 0,-44.27545 35.89234,-80.16777 80.167778,-80.16777 16.77916,0.0322 33.12527,5.32843 46.73512,15.14236 0,0 12.49925,8.02581 19.11222,0.3185 6.61295,-7.70732 -2.4058,-16.41739 -2.4058,-16.41739 C 151.84561,95.74815 129.51494,87.97828 106.4978,87.901069 Z m 54.30959,56.450221 -12.13663,11.69622 20.15864,20.93332 -93.932488,-1.4899 c -9.22763,-0.17349 -16.77655,6.07423 -16.92587,14.00904 l 0.002,0.002 c -0.0149,1.82673 -0.0235,3.40102 0,4.99598 l -0.002,0.002 c 0.14932,7.93483 7.69824,14.18254 16.92587,14.00905 l 93.932488,-1.48991 -20.15864,20.93333 12.13663,11.69622 34.0585,-35.35536 11.82982,-12.29208 h 0.003 l -9.9e-4,-0.002 9.9e-4,-9.9e-4 h -0.003 l -11.82982,-12.29208 z"
id="path1405"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccccsccsccsccscccccccccccccccccccccc" />
<path
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.39729571;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 506.57967,92.503023 c -57.98068,-1e-4 -104.98336,47.002567 -104.98326,104.983257 1.9e-4,57.98049 47.00276,104.98284 104.98326,104.98273 23.42489,-0.0758 46.15146,-7.98387 57.83458,-18.08923 11.68313,-10.10537 12.15613,-18.62993 7.38675,-23.04107 v -0.002 c -4.7711,-4.41269 -12.38099,-1.9587 -17.69245,2.25103 -13.83538,9.99805 -30.45915,15.40285 -47.52888,15.4528 -45.04116,0 -81.55421,-36.51305 -81.5542,-81.55419 0,-45.04114 36.51307,-81.5542 81.5542,-81.5542 17.06933,0.0328 33.21884,5.19482 43.16812,12.86758 9.94929,7.67275 17.33418,9.17607 22.1053,4.76338 v -0.002 c 4.77116,-4.41278 5.55882,-12.9887 -0.73482,-18.60197 -18.40654,-14.47308 -41.1234,-22.377337 -64.5386,-22.455877 z m 55.24881,57.426467 -12.34652,11.8985 20.50728,21.29534 -95.55697,-1.51567 c -9.38721,-0.17649 -17.06669,6.17929 -17.21858,14.25133 l 0.003,0.002 c -0.15192,8.07203 7.28245,14.71295 16.66978,14.88953 l 95.22519,1.50947 -21.06332,20.28455 12.04579,12.49846 36.06808,-34.74464 0.005,0.005 12.34654,-11.89954 -12.03701,-12.50724 z m 35.17874,98.71801 0.69918,0.67386 c 0.13539,-0.22412 0.26991,-0.44874 0.4036,-0.67386 z"
id="path1405-6"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccsczccccccccccccccccccccccc" />
<path
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.39729571;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 740.89945,94.730897 c -57.98068,-9.6e-5 -104.98334,47.002563 -104.98325,104.983253 1.9e-4,57.98049 47.00276,104.98284 104.98325,104.98274 23.42488,-0.0758 46.15145,-7.98387 64.5635,-22.46581 l -17.03461,-16.41553 c -13.83537,9.99805 -30.45916,15.40285 -47.52889,15.4528 -45.04113,0 -81.55419,-36.51306 -81.55419,-81.5542 0,-45.04114 36.51306,-81.55419 81.55419,-81.55419 17.06934,0.0328 33.69814,5.42058 47.54336,15.40423 l 16.99534,-16.3773 c -18.40663,-14.4732 -41.12349,-22.377447 -64.5387,-22.455993 z m 55.24882,57.426473 -12.34653,11.8985 20.50728,21.29534 -95.55696,-1.51567 c -9.38721,-0.17649 -17.06668,6.17928 -17.21858,14.25132 l 0.002,0.002 c -0.1519,8.07203 7.28245,14.71295 16.66978,14.88953 l 95.22519,1.50947 -21.06332,20.28455 12.04578,12.49846 36.06808,-34.74465 0.005,0.005 12.34653,-11.89953 -12.03699,-12.50725 z m 35.17873,98.718 0.69919,0.67386 c 0.13538,-0.22412 0.26991,-0.44874 0.40359,-0.67386 z"
id="path1405-9"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccsccccccccccccccccccccccc" />
</g>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.5.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="svg1566" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:svg="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 595.3 595.3"
style="enable-background:new 0 0 595.3 595.3;" xml:space="preserve">
<g>
<path d="M99,295.1c0,105.5,75.4,194.1,177.1,209.1c68.9,10.1,128.8-9.3,179.1-57.3c14.4-13.7,30.2-19,49-11.3
c27.8,11.3,35.3,47.2,14.3,68.5c-43.9,44.6-96.7,72.7-158.4,83.6c-65,11.4-127.4,2.9-185.7-27.8c-85.2-44.9-138.5-115-155.9-210.1
C2.5,261.9,23,182,78.9,112.4C129,50.1,194.6,13.3,274.5,4.6c94.2-10.3,175.3,18.6,243,84.9c13.3,13,16.6,31.3,9.4,47.7
c-7.1,16-23.4,26.2-40.9,25.4c-11.6-0.6-21.3-5.5-29.6-13.7c-28.7-28.3-62.6-47.1-102-56.1c-117-26.9-234.8,53.1-252.9,171.9
C99.9,275.4,98.7,286.2,99,295.1z"/>
<path d="M434.1,339.9c-2.6,0-5.2,0-7.8,0c-69,0-138.1,0-207.1,0c-19.1,0-34.3-10.6-40.8-28.2c-6-16.2-1.2-34.6,12.4-46.4
c8-7,17.5-10.2,28.3-10.2c69,0.1,138.1,0,207.1,0c2.6,0,5.2,0,8.7,0c-2.2-4.7-5.9-7-8.7-10.1c-11.1-12.6-15.3-26.9-9.9-43.1
c5.3-15.8,16.6-25.5,32.9-28.6c14.2-2.7,27.1,1.3,37.3,11.4c28,27.6,55.9,55.3,83.4,83.5c16.1,16.5,16.1,42.2,0.1,58.7
c-27.6,28.3-55.6,56.3-83.9,83.8c-12.7,12.3-28.4,15.4-44.9,8.8c-16.7-6.7-25.9-19.8-27.1-37.7c-0.8-12,3.7-22.7,12.1-31.5
c2.8-3,6.1-5.5,9.2-8.3C435,341.3,434.5,340.6,434.1,339.9z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,17 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.5.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!-- Generator: Adobe Illustrator 27.5.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="svg1303" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:svg="http://www.w3.org/2000/svg" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" inkscape:version="0.92.4 (5da689c313, 2019-01-14)" sodipodi:docname="Icons_seen.svg"
id="svg1303" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:svg="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 500 500"
style="enable-background:new 0 0 500 500;" xml:space="preserve">
<sodipodi:namedview bordercolor="#666666" borderopacity="1.0" id="base" inkscape:current-layer="layer1" inkscape:cx="84.208758" inkscape:cy="136.94831" inkscape:document-units="mm" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:window-height="1017" inkscape:window-maximized="1" inkscape:window-width="1920" inkscape:window-x="-8" inkscape:window-y="-8" inkscape:zoom="1.0105705" pagecolor="#ffffff" showgrid="false" units="px">
</sodipodi:namedview>
<g id="layer1" transform="translate(0,-164.70764)" inkscape:groupmode="layer" inkscape:label="Ebene 1">
<path id="path1091" inkscape:connector-curvature="0" d="M250,259.4c-105.2,0.1-201.5,60-250,155.4C48.5,510.1,144.9,569.9,250,570
c105.2-0.1,201.5-60,250-155.4C451.5,319.3,355.1,259.5,250,259.4z M230.7,287.8c-18.7,2.8-36.6,9.8-52.4,20.3
c2.8-0.5,5.7-0.7,8.6-0.7c27.8,0,50.4,22.6,50.4,50.4c0,0,0,0,0,0c0,27.8-22.6,50.4-50.4,50.4c0,0,0,0,0,0
c-27.8,0-50.4-22.6-50.4-50.4c0,0,0,0,0,0c0-1.2,0.1-2.4,0.2-3.6c-10,18.6-15.2,39.4-15.2,60.5c0,62.6,45,116,106.7,126.7
c-78.3-7.6-147.6-55.2-183.9-126.6C81,342.6,151.3,294.7,230.7,287.8L230.7,287.8z M271.8,288c78.3,7.6,147.6,55.2,183.9,126.6
c-36.7,72.1-106.9,120-186.2,127c62.6-9.7,108.9-63.5,108.9-126.9C378.5,352.1,333.5,298.7,271.8,288L271.8,288z"/>
<g>
<path d="M249.8,406.5C142.2,401.9,59.2,355.3,3.6,261.9c-4.7-8-4.8-16.2-0.1-24.2c46.6-79.1,115-127.3,205.9-141.1
c113.5-17.1,224.7,36.5,283.5,134.6c1,1.7,1.9,3.5,3,5.2c5.6,8.8,5.4,17.5,0.1,26.5c-29,50.1-69.2,88.2-121,113.9
c-33.4,16.6-68.7,26.2-106,28.5C262.7,405.7,256.4,406.2,249.8,406.5z M139.6,249.7c0,61.4,49.1,110.5,110.6,110.5
c61.2,0,110.3-49.1,110.4-110.2c0.1-61.5-48.9-110.7-110.4-110.7C188.7,139.2,139.6,188.3,139.6,249.7z"/>
<path d="M174.9,249.8c0-42.1,33.2-75.3,75.4-75.2c41.7,0,75,33.3,75,75.2c0,42.1-33.3,75.3-75.5,75.3
C208.1,324.9,174.9,291.6,174.9,249.8z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,75 +1,13 @@
<?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_play.svg">
<defs
id="defs1297" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="3.4343607"
inkscape:cx="51.709313"
inkscape:cy="230.85077"
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)">
<path
sodipodi:type="star"
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"
id="path1206"
sodipodi:sides="3"
sodipodi:cx="-142.60593"
sodipodi:cy="272.75879"
sodipodi:r1="30.817924"
sodipodi:r2="15.408962"
sodipodi:arg1="0.52359878"
sodipodi:arg2="1.5707963"
inkscape:flatsided="false"
inkscape:rounded="-3.46945e-18"
inkscape:randomized="0"
d="m -115.91682,288.16775 -26.68911,0 -26.6891,0 13.34455,-23.11344 13.34455,-23.11345 13.34456,23.11345 z"
transform="matrix(0,1.7014617,-1.7014617,0,530.04498,473.492)"
inkscape:transform-center-x="-18.792756" />
</g>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.5.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="svg1303" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:svg="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 500 500"
style="enable-background:new 0 0 500 500;" xml:space="preserve">
<g>
<path d="M175.5,421.6c-4,0-8,0-12.1,0c-13.9-3.4-21-13-24.1-26.3c-1-4.5-1.5-9-1.5-13.6c0-87.7,0-175.4,0-263.1
c0-5.5,0.6-10.9,2.2-16.2c3.7-12.5,11-21.5,24.5-23.6c9-1.4,17.6,0.7,25.5,5c10.9,5.9,21.6,12.1,32.4,18.2
c66.6,38.2,133.2,76.4,199.8,114.6c7.1,4.1,13.7,8.7,18.8,15.4c8.7,11.5,8.7,25,0,36.5c-5,6.6-11.7,11.3-18.8,15.4
c-74,42.5-148.1,84.9-222.1,127.4C192.2,415.7,184.4,419.8,175.5,421.6z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,63 +1,23 @@
<?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="Rescan Subscriptions.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.85859018"
inkscape:cx="125.63373"
inkscape:cy="156.7016"
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)">
<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 240.35352 18.810547 L 240.35352 29.246094 A 221.29301 221.29301 0 0 0 64.912109 129.45898 L 95.701172 150.14453 A 184.02261 184.02261 0 0 1 240.35352 66.369141 L 240.35352 87.705078 L 270.18359 70.480469 L 300.01562 53.257812 L 270.18359 36.035156 L 240.35352 18.810547 z M 312.28516 37.798828 L 301.08398 73.353516 A 184.02261 184.02261 0 0 1 422.6875 187.33594 L 406.34375 192.03125 L 431.13281 215.94922 L 455.92188 239.86719 L 464.24219 206.44141 L 472.56055 173.01367 L 458.55078 177.03711 A 221.29301 221.29301 0 0 0 312.28516 37.798828 z M 73.375 144.88281 L 48.585938 168.80078 L 23.796875 192.71875 L 35.617188 196.11328 A 221.29301 221.29301 0 0 0 28.707031 250 A 221.29301 221.29301 0 0 0 77.992188 388.9375 L 107.99805 366.83789 A 184.02261 184.02261 0 0 1 65.976562 250 A 184.02261 184.02261 0 0 1 71.474609 206.41211 L 90.013672 211.73633 L 81.693359 178.30859 L 73.375 144.88281 z M 433.95312 248.4375 A 184.02261 184.02261 0 0 1 434.02344 250 A 184.02261 184.02261 0 0 1 367.40625 391.49219 L 358.92773 380.14844 L 345.34375 411.80469 L 331.75977 443.45898 L 365.9668 439.39648 L 400.17188 435.33203 L 389.71484 421.33984 A 221.29301 221.29301 0 0 0 471.29297 250 A 221.29301 221.29301 0 0 0 471.23047 248.77539 L 433.95312 248.4375 z M 94.9375 385.80273 L 108.97852 417.25781 L 123.02148 448.71289 L 131.60156 436.87891 A 221.29301 221.29301 0 0 0 250 471.29297 A 221.29301 221.29301 0 0 0 328.64648 456.57227 L 315.99023 421.74805 A 184.02261 184.02261 0 0 1 250 434.02344 A 184.02261 184.02261 0 0 1 153.63867 406.48047 L 163.45898 392.93555 L 129.19727 389.36914 L 94.9375 385.80273 z "
transform="matrix(0.26458394,0,0,0.26458394,0,164.70749)"
id="path826" />
</g>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.5.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="svg1303" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:svg="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 500 500"
style="enable-background:new 0 0 500 500;" xml:space="preserve">
<sodipodi:namedview bordercolor="#666666" borderopacity="1.0" id="base" inkscape:current-layer="layer1" inkscape:cx="125.63373" inkscape:cy="156.7016" inkscape:document-units="mm" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:window-height="1017" inkscape:window-maximized="1" inkscape:window-width="1920" inkscape:window-x="-8" inkscape:window-y="-8" inkscape:zoom="0.85859018" pagecolor="#ffffff" showgrid="false" units="px">
</sodipodi:namedview>
<g>
<path d="M269,28.7c10.8,1.6,21.6,2.8,32.3,5.3c38.3,9.2,71.9,27.2,100.7,54.1c3.4,3.2,4.7,2.4,6.8-1.1c3.3-5.5,7.1-10.9,10.7-16.2
c4.8-7,11.3-9.7,18.5-7.6c6.8,1.9,11.5,8.2,11.5,16.5c-0.1,26.3,0.3,52.7-0.5,79c-0.4,12.5-0.1,25-0.2,37.6
c-0.1,14-11.1,21.3-23.9,15.6c-35.2-15.7-70.3-31.6-105.5-47.4c-7.7-3.5-11.8-10.1-10.8-16.9c1.1-7.8,6.9-13.2,15.6-14.4
c4-0.6,8-1.2,11.9-1.8c0.9-0.1,2.3,0.2,2.6-1.1c0.3-1.2-1-1.6-1.8-2.2c-12.2-8.9-25.5-15.7-39.8-20.3
c-72.7-23.2-148.9,9.9-181.6,78.9c-4.1,8.7-8.3,17-17.1,22.1c-13.7,8-30.1,6.6-42.2-3.8c-11.3-9.6-15.6-26.6-9.6-40.5
c15.7-36.9,39.4-67.7,71.6-91.8c29.8-22.4,63.2-36.3,100-41.9c4.8-0.7,9.7-1.3,14.5-2C244.8,28.7,256.9,28.7,269,28.7z"/>
<path d="M52.1,361.3c0.6-20,0.8-39,0.8-58c0-14,11.1-21.2,23.9-15.5c25.2,11.2,50.4,22.6,75.6,33.9c10.1,4.5,20.2,9,30.3,13.7
c6.4,2.9,10.5,7.8,10.5,15c0,6.4-3.2,11.2-9,14.2c-3.8,2-8.1,2-12.1,2.8c-2.1,0.4-4.3,0.6-6.4,1c-0.9,0.1-2.3-0.2-2.6,1.1
c-0.3,1.2,1,1.6,1.8,2.2c12.2,8.9,25.5,15.7,39.8,20.3c72.7,23.2,148.9-9.9,181.6-78.9c4.2-8.8,8.5-17.3,17.4-22.4
c13.6-7.7,29.8-6.3,41.8,4c11.3,9.6,15.6,26.6,9.7,40.5c-15.7,36.9-39.4,67.7-71.6,91.8c-29.7,22.3-62.9,36.5-99.6,41.8
c-57.8,8.3-110.9-4-159.1-36.9c-9.3-6.3-17.9-13.5-26.1-21.2c-2.1-2-3.3-2.1-4.9,0.5c-3.4,5.5-7.1,10.8-10.7,16.2
c-5.7,8.5-12.2,11.4-19.9,8.9c-7.3-2.3-11.1-8.6-11.1-18.5C52.1,398.6,52.1,379.4,52.1,361.3z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -1,64 +1,15 @@
<?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_Lupe.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="-23.955221"
inkscape:cy="207.58555"
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)">
<path
style="opacity:1;fill:#002130;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 15.593586,179.85791 c 18.094683,-18.09394 47.431291,-18.09355 65.52552,7.7e-4 16.496472,16.53809 18.102694,42.7696 3.747207,61.19678 17.821687,15.21024 24.490877,18.93088 42.171347,34.20969 4.41254,4.71717 4.29213,12.08299 -0.27223,16.65347 -4.57089,4.56126 -11.93421,4.68001 -16.65007,0.26874 l -8.4e-4,9.1e-4 -0.0173,-0.0152 C 94.335453,273.50898 90.211408,265.24542 76.792795,249.15281 58.360539,263.5048 32.12693,261.89037 15.593512,245.38662 c -18.0947127,-18.09455 -18.0947127,-47.43175 0,-65.52636 z m 7.908493,7.90846 c -13.7275962,13.72683 -13.7279739,35.98299 -8.23e-4,49.71019 13.72719,13.72714 35.983444,13.72678 49.710173,-8.2e-4 13.726834,-13.72692 13.726834,-35.98245 0,-49.70937 -13.726863,-13.72676 -35.982478,-13.72676 -49.70935,0 z"
id="rect826"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccccccccccc" />
</g>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.5.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="svg1303" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:svg="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 500 500"
style="enable-background:new 0 0 500 500;" xml:space="preserve">
<g>
<path d="M492.4,462.2c-1,2.5-1.8,5.1-3,7.6C478,495,448.2,501.9,427.2,484c-1.8-1.5-3.5-3.2-5.2-4.9
c-34.1-34.8-68.3-69.5-102.3-104.4c-3.3-3.4-5.6-3.6-9.5-1c-30.5,20.3-64.1,31.1-100.6,32c-60.1,1.5-110.4-20.8-151.1-65.6
c-28.2-31-45.1-67.8-49.5-109.8C1.6,161.2,23.6,102.8,74.3,56.1c28.7-26.4,62.7-42.4,101-47.9c52.7-7.6,101,4.5,143.9,36.5
c43.7,32.6,70,76.6,78.4,131c7.1,45.8-1.1,89.2-23.7,129.6c-3.7,6.6-3.8,6.6,1.5,11.9c33.3,34,66.6,68,100.1,101.9
c7.6,7.7,14.2,15.9,16.9,26.7C494.1,451.5,493.6,456.9,492.4,462.2z M335.2,205.9c0.5-72.8-58.3-134.2-131.9-134
C132.9,72,72,129.3,72,206c0,74.8,58.1,134.2,131.7,134.2C278.8,340.2,335.8,278.1,335.2,205.9z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,67 +1,21 @@
<?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>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.5.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="svg1303" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:svg="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 500 500"
style="enable-background:new 0 0 500 500;" xml:space="preserve">
<g>
<path d="M147.7,416.2c-8.7-3.4-11.9-8.1-11.9-17.5c0-53.5,0-107,0-160.5c0-9.3-4.2-15-12.6-16.7c-1.7-0.4-3.4-0.4-5.2-0.4
c-8.3,0-16.6,0-25,0c-7.3,0-12.9-3-16-9.5c-3.1-6.6-2.4-13,2.3-18.8c21.1-26.4,42.1-52.9,63.2-79.4c5.7-7.2,11.4-14.4,17.1-21.6
c3.2-4.1,7-7.4,12.5-7.9c6.6-0.6,11.4,2.3,15.3,7.3c5.7,7.4,11.5,14.9,17.3,22.3c20.5,26.4,40.9,52.8,61.5,79
c7.2,9.2,4.3,19.8-2,24.9c-3.2,2.6-6.8,3.6-10.8,3.6c-8.6,0-17.3,0-25.9,0c-10.6,0.1-16.2,5.6-16.2,16.2
c0,53.1-0.1,106.1,0.1,159.2c0,9.6-3.4,16.3-12.6,19.7C181.9,416.2,164.8,416.2,147.7,416.2z"/>
</g>
<g>
<path d="M369.5,83.8c8.7,3.4,11.9,8.1,11.9,17.5c0,53.5,0,106.9,0,160.4c0,9.3,4.2,15,12.6,16.7c1.7,0.3,3.4,0.4,5.2,0.4
c8.3,0,16.6,0,25,0c7.3,0,12.9,3,16,9.5c3.1,6.6,2.4,13-2.3,18.8c-21.1,26.4-42.1,52.9-63.2,79.3c-5.7,7.2-11.4,14.4-17.1,21.6
c-3.2,4.1-6.9,7.4-12.5,7.9c-6.6,0.6-11.4-2.3-15.3-7.3c-5.7-7.4-11.5-14.8-17.3-22.3c-20.5-26.4-40.8-52.8-61.5-79
c-7.2-9.2-4.3-19.8,2-24.9c3.2-2.6,6.8-3.6,10.8-3.6c8.6,0,17.3,0,25.9,0c10.6-0.1,16.2-5.6,16.2-16.2c0-53,0.1-106.1-0.1-159.1
c0-9.6,3.4-16.3,12.6-19.7C335.4,83.8,352.5,83.8,369.5,83.8z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,65 +1,25 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="500"
height="500"
viewBox="0 0 132.29197 132.29167"
version="1.1"
id="svg1303"
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
sodipodi:docname="icon-star-empty.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs1297" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="2.4005076"
inkscape:cx="40.40812"
inkscape:cy="211.20533"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:window-width="3840"
inkscape:window-height="2112"
inkscape:window-x="0"
inkscape:window-y="48"
inkscape:window-maximized="1"
inkscape:pagecheckerboard="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 />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Ebene 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-164.70764)">
<path
id="path1255"
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"
inkscape:transform-center-y="-6.4703827"
d="M 250 21.158203 L 189.78906 194.37891 L 6.4375 198.11523 L 152.57617 308.9082 L 99.470703 484.43945 L 250 379.69141 L 400.5293 484.43945 L 347.42383 308.9082 L 493.5625 198.11523 L 310.21094 194.37891 L 250 21.158203 z M 250 96.408203 L 291.69141 216.34766 L 418.64453 218.93555 L 317.45703 295.65039 L 354.22852 417.18945 L 250 344.66211 L 145.77148 417.18945 L 182.54297 295.65039 L 81.355469 218.93555 L 208.30859 216.34766 L 250 96.408203 z "
transform="matrix(0.26458394,0,0,0.26458394,0,164.70749)" />
</g>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.5.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="svg1303" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:svg="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 500 500"
style="enable-background:new 0 0 500 500;" xml:space="preserve">
<g>
<path d="M492.6,214.8c-2.7,7.3-7.9,12.7-13.8,17.7c-32.6,28.4-65.1,57-97.7,85.5c-2.4,2.1-2.4,4.1-1.8,6.7
c9.9,43.5,19.8,87.1,29.7,130.6c3.8,16.8-4.6,30.8-20.6,34.1c-7.2,1.5-13.7-0.8-19.8-4.4c-37.7-22.5-75.4-45-113-67.7
c-4.2-2.6-7.2-2.5-11.4,0.1c-37.7,22.8-75.6,45.3-113.4,67.9c-17.6,10.5-37.4,1.6-40.9-18.2c-0.7-3.8,0-7.5,0.8-11.2
c9.8-43.4,19.7-86.8,29.6-130.2c0.8-3.7,0.4-5.9-2.6-8.5c-33.2-28.9-66.3-58.1-99.5-87c-4.9-4.3-8.5-9.3-10.9-15.2
c-1-4.4-1.4-8.8,0-13.3c4.9-13.7,14.9-19.5,29.3-20.4c22.5-1.4,44.9-4,67.4-6.1c19-1.7,38-3.6,57-5c6.3-0.5,10.3-2.2,13-8.8
c16.4-39.3,33.3-78.3,50-117.4c2.6-6.2,5.7-12,11.5-15.8c13.6-8.9,31.4-4.1,38.3,11.1c8.8,19.4,17,39.1,25.3,58.7
c9.4,22.1,18.9,44.1,28.2,66.2c1.4,3.3,3.3,4.9,6.9,5.2c15.9,1.3,31.7,2.8,47.6,4.3c23.1,2.1,46.2,4.2,69.2,6.4
c4.2,0.4,8.5,1.1,12.7,1.1c14.3,0.2,23.9,6.8,28.8,20.3C494,206,493.8,210.4,492.6,214.8z M50.8,211.9c-1.9,0.3-2.5,2.7-1.1,3.9
c0.1,0.1,0.2,0.2,0.3,0.3c16.9,14.8,33.7,29.6,50.6,44.4c14.2,12.5,28.5,25,42.7,37.5c8.8,7.7,11.7,17.1,9,28.6
c-8.4,36.3-16.6,72.6-24.8,108.9c-0.9,4.1-2.1,8.1-2.8,12.2c-0.3,2.1,2.1,3.6,3.8,2.4c0.4-0.3,0.8-0.5,1.1-0.8
c34.9-20.8,69.7-41.6,104.5-62.4c3.7-2.2,7.4-4,11.8-4.7c8.6-1.4,15.6,2.2,22.7,6.4c34.1,20.5,68.3,40.9,102.5,61.3
c1.2,0.7,2.6,2.6,4.1,1.3c1.1-1,0-2.7-0.3-4c-9-39.7-17.7-79.5-27-119.1c-3-12.8,0.1-22.8,9.9-31.3c24.3-21.1,48.5-42.4,72.7-63.6
c6.7-5.8,13.3-11.7,20.2-17.8c1.4-1.2,0.7-3.6-1.2-3.8c0,0,0,0,0,0c-17.9-1.6-35.8-3.3-53.7-4.8c-22.6-2-45.2-4.2-67.9-5.9
c-14.2-1.1-22.9-8-28.4-21.2c-15.4-37.2-31.5-74.1-47.3-111.1c-0.5-1.2-0.6-3.2-2.3-3.2s-1.8,2-2.3,3.2c-7.7,18-15.4,36-23.1,54
c-8.6,20.2-17.2,40.4-25.9,60.6c-4,9.2-10.6,15.6-20.7,16.7c-20.2,2.3-40.5,4-60.8,5.8c-19,1.7-38,3.2-57,5.1
C57.2,211,54.2,211.3,50.8,211.9z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -1,74 +1,19 @@
<?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_star.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="61.891881"
inkscape:cy="148.25167"
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)">
<path
sodipodi:type="star"
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"
id="path1255"
sodipodi:sides="5"
sodipodi:cx="66.145981"
sodipodi:cy="237.32387"
sodipodi:r1="67.758904"
sodipodi:r2="27.103561"
sodipodi:arg1="-1.5707963"
sodipodi:arg2="-0.9424778"
inkscape:flatsided="false"
inkscape:rounded="-3.46945e-18"
inkscape:randomized="0"
d="m 66.145983,169.56496 15.931071,45.83167 48.511476,0.98859 L 91.923,245.69933 105.97366,292.14197 66.145981,264.42743 26.318295,292.14197 40.368962,245.69933 1.7034347,216.38521 50.214907,215.39663 Z"
inkscape:transform-center-y="-6.4703827" />
</g>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.5.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="svg1303" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:svg="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 500 500"
style="enable-background:new 0 0 500 500;" xml:space="preserve">
<sodipodi:namedview bordercolor="#666666" borderopacity="1.0" id="base" inkscape:current-layer="layer1" inkscape:cx="61.891881" inkscape:cy="148.25167" inkscape:document-units="mm" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:window-height="1017" inkscape:window-maximized="1" inkscape:window-width="1920" inkscape:window-x="-8" inkscape:window-y="-8" inkscape:zoom="1.0105705" pagecolor="#ffffff" showgrid="false" units="px">
</sodipodi:namedview>
<g>
<path d="M493.2,207.8c-2.7,7.3-7.9,12.7-13.8,17.7c-32.6,28.4-65.1,57-97.7,85.5c-2.4,2.1-2.4,4.1-1.8,6.7
c9.9,43.5,19.8,87.1,29.7,130.6c3.8,16.8-4.6,30.8-20.6,34.1c-7.2,1.5-13.7-0.8-19.8-4.4c-37.7-22.5-75.4-45-113-67.7
c-4.2-2.6-7.2-2.5-11.4,0.1c-37.7,22.8-75.6,45.3-113.4,67.9c-17.6,10.5-37.4,1.6-40.9-18.2c-0.7-3.8,0-7.5,0.8-11.2
c9.8-43.4,19.7-86.8,29.6-130.2c0.8-3.7,0.4-5.9-2.6-8.5C85.1,281.1,52,252,18.8,223c-4.9-4.3-8.5-9.3-10.9-15.2
c-1-4.4-1.4-8.8,0-13.3c4.9-13.7,14.9-19.5,29.3-20.4c22.5-1.4,44.9-4,67.4-6.1c19-1.7,38-3.6,57-5c6.3-0.5,10.3-2.2,13-8.8
c16.3-39.2,33.2-78.2,49.8-117.2c2.6-6.2,5.7-12,11.5-15.8c13.6-8.9,31.4-4.1,38.3,11.1c8.8,19.4,17,39.1,25.3,58.7
c9.4,22.1,18.9,44.1,28.2,66.2c1.4,3.3,3.3,4.9,6.9,5.2c15.9,1.3,31.7,2.8,47.6,4.3c23.1,2.1,46.2,4.2,69.2,6.4
c4.2,0.4,8.5,1.1,12.7,1.1c14.3,0.2,23.9,6.8,28.8,20.3C494.5,199,494.3,203.4,493.2,207.8z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -1,66 +1,22 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="500"
height="500"
viewBox="0 0 132.29197 132.29167"
version="1.1"
id="svg1303"
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
sodipodi:docname="icon-star-half.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<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="44.034533"
inkscape:cy="168.22181"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:window-width="3840"
inkscape:window-height="2078"
inkscape:window-x="0"
inkscape:window-y="82"
inkscape:window-maximized="1"
inkscape:pagecheckerboard="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 />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Ebene 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-164.70764)">
<path
id="path1255"
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"
inkscape:transform-center-x="1.1220981e-06"
inkscape:transform-center-y="-6.4703827"
d="M 250 21.158203 L 189.78906 194.37891 L 6.4375 198.11523 L 152.57617 308.9082 L 99.470703 484.43945 L 250 379.69141 L 400.5293 484.43945 L 347.42383 308.9082 L 493.5625 198.11523 L 310.21094 194.37891 L 250 21.158203 z M 250 96.408203 L 291.69141 216.34766 L 418.64453 218.93555 L 317.45703 295.65039 L 354.22852 417.18945 L 250 344.66211 L 250 96.408203 z "
transform="matrix(0.26458394,0,0,0.26458394,0,164.70749)" />
</g>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.5.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="svg1303" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:cc="http://creativecommons.org/ns#"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 500 500"
style="enable-background:new 0 0 500 500;" xml:space="preserve">
<g>
<path d="M7.5,207.8c2.8,7.3,8,12.6,13.8,17.7C54,253.9,86.4,282.6,119,311c2.4,2.1,2.4,4.1,1.8,6.7c-9.9,43.5-19.8,87.1-29.7,130.6
c-3.9,16.9,4.6,30.9,20.6,34.1c7.2,1.5,13.7-0.7,19.8-4.4c37.7-22.6,75.4-45,112.9-68c1.9-1.3,3.7-1.9,5.4-1.9v0.3
c1.9,0,3.7,0.6,5.8,1.9c37.7,22.8,75.5,45.4,113.4,67.9c17.6,10.5,37.4,1.6,40.9-18.2c0.7-3.8,0-7.5-0.8-11.2
c-9.8-43.4-19.6-86.8-29.6-130.2c-0.9-3.6-0.4-5.9,2.6-8.5c33.3-28.9,66.3-58,99.5-87c4.9-4.3,8.5-9.3,10.9-15.2
c1-4.5,1.4-8.9,0-13.3c-5-13.7-14.9-19.5-29.3-20.4c-22.5-1.4-44.9-4.1-67.4-6.1c-19-1.8-38-3.6-57-5c-6.3-0.5-10.2-2.2-13-8.8
c-16.2-39.2-33.2-78.1-49.8-117.2c-2.6-6.2-5.7-12-11.5-15.8c-4.6-3-9.5-4.5-14.5-4.5v-0.1c-9.8,0-19.2,5.5-23.8,15.6
c-8.8,19.4-16.9,39.1-25.3,58.7c-9.5,22-18.9,44.1-28.2,66.2c-1.4,3.3-3.3,4.9-6.9,5.2c-15.9,1.3-31.7,2.8-47.6,4.3
c-23,2.2-46.1,4.3-69.2,6.4c-4.2,0.4-8.5,1-12.7,1.1C22,174.4,12.4,181,7.5,194.5C6.1,199,6.3,203.4,7.5,207.8z M250,374.9V58.2
c1.6,0.1,1.7,2,2.2,3.2c7.7,18,15.4,36,23.1,54c8.7,20.2,17.3,40.4,25.9,60.6c3.9,9.2,10.6,15.6,20.7,16.7
c20.3,2.3,40.5,4,60.8,5.8c19,1.7,38,3.1,57,5.1l6,0.6c3.3,0.3,4.6,4.3,2.1,6.5c-15.6,13.8-33.6,29.7-48.4,42.7
c-14.2,12.5-28.5,25-42.7,37.5c-8.7,7.7-11.6,17.2-9,28.6c8.4,36.2,16.6,72.6,24.8,108.9c0.9,4,2.1,8,2.8,12.1
c0.3,2.2-2.1,3.7-3.9,2.5c-0.4-0.3-0.7-0.5-1.1-0.7c-34.8-20.8-69.7-41.6-104.5-62.4c-3.7-2.2-7.5-4-11.8-4.7
C252.8,375,251.6,374.9,250,374.9L250,374.9z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -1,75 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="474.99609"
height="78.428696"
viewBox="0 0 125.67634 20.750926"
version="1.1"
id="svg1303"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="icon-add.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs1297" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.85859018"
inkscape:cx="-97.380081"
inkscape:cy="261.09215"
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"
showguides="true"
inkscape:guide-bbox="true"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1">
<sodipodi:guide
position="-225.18364,87.524084"
orientation="1,0"
id="guide1072"
inkscape:locked="false" />
</sodipodi:namedview>
<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(-3.3077777,-220.4781)">
<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 12.291019,220.47796 c -4.9648228,-0.009 -8.9691711,3.98046 -8.9784054,8.94536 l -0.00482,2.62846 c -0.00925,4.9649 3.9804459,8.96933 8.9452674,8.97832 107.703889,0.29274 22.266017,0.0414 107.747629,0.19881 4.96484,0.009 8.96917,-3.98046 8.9784,-8.94534 l 0.005,-2.62847 c 0.009,-4.96489 -3.98037,-8.96923 -8.94525,-8.97831 -107.850822,-0.0255 -20.413204,-0.038 -107.747821,-0.19883 z"
id="rect1073"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccc" />
</g>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.5.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="svg1303" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:svg="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 475 111.2"
style="enable-background:new 0 0 475 111.2;" xml:space="preserve">
<sodipodi:namedview bordercolor="#666666" borderopacity="1.0" id="base" inkscape:current-layer="layer1" inkscape:cx="-97.380081" inkscape:cy="261.09215" inkscape:deskcolor="#d1d1d1" inkscape:document-units="mm" inkscape:guide-bbox="true" inkscape:pagecheckerboard="0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:showpageshadow="2" inkscape:window-height="1017" inkscape:window-maximized="1" inkscape:window-width="1920" inkscape:window-x="-8" inkscape:window-y="-8" inkscape:zoom="0.85859018" pagecolor="#ffffff" showgrid="false" showguides="true" units="px">
<sodipodi:guide id="guide1072" inkscape:locked="false" orientation="1,0" position="-225.18364,87.524084"></sodipodi:guide>
</sodipodi:namedview>
<path d="M181.9,0H55.6C25,0,0,25,0,55.6l0,0c0,30.6,25,55.6,55.6,55.6h126.3h111.2h126.3c30.6,0,55.6-25,55.6-55.6l0,0
C475,25,450,0,419.4,0H293.1H181.9z"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,18 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.5.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!-- Generator: Adobe Illustrator 27.5.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="svg1303" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:svg="http://www.w3.org/2000/svg" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" inkscape:version="0.92.4 (5da689c313, 2019-01-14)" sodipodi:docname="icon_thumb.svg"
id="svg1303" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:svg="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 500 500"
style="enable-background:new 0 0 500 500;" xml:space="preserve">
<sodipodi:namedview bordercolor="#666666" borderopacity="1.0" id="base" inkscape:current-layer="layer1" inkscape:cx="35.718548" inkscape:cy="203.39339" inkscape:document-units="mm" inkscape:guide-bbox="true" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:window-height="1009" inkscape:window-maximized="1" inkscape:window-width="1920" inkscape:window-x="-8" inkscape:window-y="-8" inkscape:zoom="1.5046797" pagecolor="#ffffff" showgrid="false" showguides="true" units="px">
<sodipodi:guide id="guide816" inkscape:locked="false" orientation="0,1" position="80.99058,65.965029"></sodipodi:guide>
<sodipodi:guide id="guide818" inkscape:locked="false" orientation="1,0" position="65.965178,49.107299"></sodipodi:guide>
</sodipodi:namedview>
<g id="layer1" transform="translate(0,-164.70764)" inkscape:groupmode="layer" inkscape:label="Ebene 1">
<path id="rect1278" inkscape:connector-curvature="0" d="M285.3,164.7c-3.1-0.1-6.4,0.1-9.8,0.7c-73.7,12.2,24,119.6-61.8,180.9
c-71.7,59.8-97.8,41.1-98.5,60.7v192.6c10.8,28.5,82.3,57.6,158.4,63.8c76.1,6.2,182.5-9.9,183.6-43c0.6-30-16.5-30.7-16.5-30.7
s37.5-2.3,38.7-34.8c0.6-31.9-36.3-36.4-36.3-36.4s47.3-1.3,47.3-34.2c0-33.6-36.4-36.3-47.6-36.8c7-0.4,34.5-10,34.4-43.2
c-0.2-34.3-52.8-31.5-147.3-37.7C340.8,302.9,348.4,166.8,285.3,164.7L285.3,164.7z M25.6,382.8c-8.9,0-16,6-16,13.4v217.5
c0,7.4,7.1,13.4,16,13.4h59.8c8.9,0,16-6,16-13.4V396.2c0-7.4-7.1-13.4-16-13.4H25.6z"/>
<g>
<path d="M142.3,330.2c0-38.6,0.1-77.1,0-115.7c-0.1-16.2,4.1-31,12.8-44.7c29.5-46.9,58.7-94,88.1-141C256.7,7.3,281-3.8,304.5,0.5
c25.2,4.7,43.6,22.6,48.9,47.9c2.4,11.8,0.8,23.2-3.4,34.3c-12.5,33.1-24.8,66.3-37.2,99.4c-2.7,7.1-2.6,7.1,5.2,7.1
c40.9,0,81.9-0.2,122.8,0.1c21.5,0.1,37.7,9.5,46.8,29.6c4.4,9.8,4.4,20,2.7,30.5c-8.1,52.1-15.9,104.3-24.2,156.4
c-2.6,16.4-3.3,33.3-9.6,48.9c-10.7,26.5-36.5,44.1-65,44.2c-66.5,0.1-133,0.1-199.5,0c-29,0-49.6-20.7-49.7-49.9
C142.3,409.6,142.3,369.9,142.3,330.2z"/>
<path d="M0,338.5c0-37.3-0.1-74.5,0-111.8c0.1-23.5,14.9-43.1,37.5-47.5c15.4-3,31.5-3.1,46.9,0.4c21.7,5,35.5,24,35.6,49.1
c0.3,73.2,0.3,146.5,0,219.7c-0.1,26.4-15.1,45.5-38.9,49.7c-13.8,2.5-28.2,2.4-42.1,0.1c-23.3-3.9-39-23.9-39.1-47.9
C-0.1,413,0,375.8,0,338.5z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -2,9 +2,11 @@
/* globals checkMessages */
function sortChange(sortValue) {
let payload = JSON.stringify({ sort_order: sortValue });
sendPost(payload);
function sortChange(button) {
let apiEndpoint = '/api/config/user/';
let data = {};
data[button.name] = button.value;
apiRequest(apiEndpoint, 'POST', data);
setTimeout(function () {
location.reload();
}, 500);
@@ -105,17 +107,21 @@ function subscribeStatus(subscribeButton) {
function changeView(image) {
let sourcePage = image.getAttribute('data-origin');
let newView = image.getAttribute('data-value');
let payload = JSON.stringify({ change_view: sourcePage + ':' + newView });
sendPost(payload);
let apiEndpoint = '/api/config/user/';
let data = {};
data[`view_style_${sourcePage}`] = newView;
console.log(data);
apiRequest(apiEndpoint, 'POST', data);
setTimeout(function () {
location.reload();
}, 500);
}
function changeGridItems(image) {
let operator = image.getAttribute('data-value');
let payload = JSON.stringify({ change_grid: operator });
sendPost(payload);
let newGridItems = Number(image.getAttribute('data-value'));
let apiEndpoint = '/api/config/user/';
let data = { grid_items: newGridItems };
apiRequest(apiEndpoint, 'POST', data);
setTimeout(function () {
location.reload();
}, 500);
@@ -123,12 +129,10 @@ function changeGridItems(image) {
function toggleCheckbox(checkbox) {
// pass checkbox id as key and checkbox.checked as value
let toggleId = checkbox.id;
let toggleVal = checkbox.checked;
let payloadDict = {};
payloadDict[toggleId] = toggleVal;
let payload = JSON.stringify(payloadDict);
sendPost(payload);
let apiEndpoint = '/api/config/user/';
let data = {};
data[checkbox.id] = checkbox.checked;
apiRequest(apiEndpoint, 'POST', data);
setTimeout(function () {
let currPage = window.location.pathname;
window.location.replace(currPage);
@@ -283,7 +287,7 @@ function reEmbed() {
}
function dbBackup() {
let apiEndpoint = '/api/task-name/run_backup/';
let apiEndpoint = '/api/backup/';
apiRequest(apiEndpoint, 'POST');
// clear button
let message = document.createElement('p');
@@ -299,8 +303,8 @@ function dbBackup() {
function dbRestore(button) {
let fileName = button.getAttribute('data-id');
let payload = JSON.stringify({ 'db-restore': fileName });
sendPost(payload);
let apiEndpoint = `/api/backup/${fileName}/`;
apiRequest(apiEndpoint, 'POST');
// clear backup row
let message = document.createElement('p');
message.innerText = 'restoring from backup';
@@ -470,7 +474,7 @@ function createPlayer(button) {
// If cast integration is enabled create cast button
let castButton = '';
if (videoData.config.application.enable_cast) {
if (videoData.config.enable_cast) {
castButton = `<google-cast-launcher id="castbutton"></google-cast-launcher>`;
}
@@ -542,7 +546,7 @@ function createVideoTag(videoData, videoProgress) {
}
let videoTag = `
<video poster="${videoThumbUrl}" ontimeupdate="onVideoProgress()" onpause="onVideoPause()" onended="onVideoEnded()" controls autoplay width="100%" playsinline id="video-item">
<video poster="${videoThumbUrl}" onvolumechange="onVolumeChange(this)" onloadstart="this.volume=getPlayerVolume()" ontimeupdate="onVideoProgress()" onpause="onVideoPause()" onended="onVideoEnded()" controls autoplay width="100%" playsinline id="video-item">
<source src="${videoUrl}#t=${videoProgress}" type="video/mp4" id="video-source" videoid="${videoId}">
${subtitles}
</video>
@@ -550,6 +554,14 @@ function createVideoTag(videoData, videoProgress) {
return videoTag;
}
function onVolumeChange(videoTag) {
localStorage.setItem('playerVolume', videoTag.volume);
}
function getPlayerVolume() {
return localStorage.getItem('playerVolume') ?? 1;
}
// Gets video tag
function getVideoPlayer() {
let videoElement = document.getElementById('video-item');
@@ -1304,14 +1316,6 @@ function populateEmpty() {
// generic
function sendPost(payload) {
let http = new XMLHttpRequest();
http.open('POST', '/process/', true);
http.setRequestHeader('X-CSRFToken', getCookie('csrftoken'));
http.setRequestHeader('Content-type', 'application/json');
http.send(payload);
}
function getCookie(c_name) {
if (document.cookie.length > 0) {
let c_start = document.cookie.indexOf(c_name + '=');

View File

@@ -5,72 +5,183 @@
/* globals apiRequest */
function primaryStats() {
let apiEndpoint = '/api/stats/primary/';
let responseData = apiRequest(apiEndpoint, 'GET');
let primaryBox = document.getElementById('primaryBox');
clearLoading(primaryBox);
let videoTile = buildVideoTile(responseData);
primaryBox.appendChild(videoTile);
let channelTile = buildChannelTile(responseData);
primaryBox.appendChild(channelTile);
let playlistTile = buildPlaylistTile(responseData);
primaryBox.appendChild(playlistTile);
let downloadTile = buildDownloadTile(responseData);
primaryBox.appendChild(downloadTile);
let apiVideoEndpoint = '/api/stats/video/';
let responseData = apiRequest(apiVideoEndpoint, 'GET');
let activeBox = document.getElementById('activeBox');
clearLoading(activeBox);
let totalTile = buildTotalVideoTile(responseData);
activeBox.appendChild(totalTile);
let activeTile = buildActiveVideoTile(responseData);
activeBox.appendChild(activeTile);
let inActiveTile = buildInActiveVideoTile(responseData);
activeBox.appendChild(inActiveTile);
let videoTypeBox = document.getElementById('videoTypeBox');
clearLoading(videoTypeBox);
let videosTypeTile = buildVideosTypeTile(responseData);
videoTypeBox.appendChild(videosTypeTile);
let shortsTypeTile = buildShortsTypeTile(responseData);
videoTypeBox.appendChild(shortsTypeTile);
let streamsTypeTile = buildStreamsTypeTile(responseData);
videoTypeBox.appendChild(streamsTypeTile);
}
function clearLoading(dashBox) {
dashBox.querySelector('#loading').remove();
function secondaryStats() {
let apiChannelEndpoint = '/api/stats/channel/';
let channelResponseData = apiRequest(apiChannelEndpoint, 'GET');
let secondaryBox = document.getElementById('secondaryBox');
clearLoading(secondaryBox);
let channelTile = buildChannelTile(channelResponseData);
secondaryBox.appendChild(channelTile);
let apiPlaylistEndpoint = '/api/stats/playlist/';
let playlistResponseData = apiRequest(apiPlaylistEndpoint, 'GET');
let playlistTile = buildPlaylistTile(playlistResponseData);
secondaryBox.appendChild(playlistTile);
let apiDownloadEndpoint = '/api/stats/download/';
let downloadResponseData = apiRequest(apiDownloadEndpoint, 'GET');
let downloadTile = buildDownloadTile(downloadResponseData);
secondaryBox.appendChild(downloadTile);
}
function buildTile(titleText) {
let tile = document.createElement('div');
tile.classList.add('info-box-item');
let title = document.createElement('h3');
title.innerText = titleText;
tile.appendChild(title);
function buildTotalVideoTile(responseData) {
const totalCount = responseData.doc_count || 0;
const totalSize = humanFileSize(responseData.media_size || 0);
const content = {
Videos: `${totalCount}`,
'Media Size': `${totalSize}`,
Duration: responseData.duration_str,
};
const tile = buildTile('All: ');
const table = buildTileContenTable(content, 2);
tile.appendChild(table);
return tile;
}
function buildVideoTile(responseData) {
let tile = buildTile(`Total Videos: ${responseData.videos.total || 0}`);
let message = document.createElement('p');
message.innerHTML = `
videos: ${responseData.videos.videos || 0}<br>
shorts: ${responseData.videos.shorts || 0}<br>
streams: ${responseData.videos.streams || 0}<br>
`;
tile.appendChild(message);
function buildActiveVideoTile(responseData) {
const activeCount = responseData?.active_true?.doc_count || 0;
const activeSize = humanFileSize(responseData?.active_true?.media_size || 0);
const duration = responseData?.active_true?.duration_str || 'NA';
const content = {
Videos: `${activeCount}`,
'Media Size': `${activeSize}`,
Duration: duration,
};
const tile = buildTile('Active: ');
const table = buildTileContenTable(content, 2);
tile.appendChild(table);
return tile;
}
function buildInActiveVideoTile(responseData) {
const inActiveCount = responseData?.active_false?.doc_count || 0;
const inActiveSize = humanFileSize(responseData?.active_false?.media_size || 0);
const duration = responseData?.active_false?.duration_str || 'NA';
const content = {
Videos: `${inActiveCount}`,
'Media Size': `${inActiveSize}`,
Duration: duration,
};
const tile = buildTile('Inactive: ');
const table = buildTileContenTable(content, 2);
tile.appendChild(table);
return tile;
}
function buildVideosTypeTile(responseData) {
const videosCount = responseData?.type_videos?.doc_count || 0;
const videosSize = humanFileSize(responseData?.type_videos?.media_size || 0);
const duration = responseData?.type_videos?.duration_str || 'NA';
const content = {
Videos: `${videosCount}`,
'Media Size': `${videosSize}`,
Duration: duration,
};
const tile = buildTile('Regular Videos: ');
const table = buildTileContenTable(content, 2);
tile.appendChild(table);
return tile;
}
function buildShortsTypeTile(responseData) {
const shortsCount = responseData?.type_shorts?.doc_count || 0;
const shortsSize = humanFileSize(responseData?.type_shorts?.media_size || 0);
const duration = responseData?.type_shorts?.duration_str || 'NA';
const content = {
Videos: `${shortsCount}`,
'Media Size': `${shortsSize}`,
Duration: duration,
};
const tile = buildTile('Shorts: ');
const table = buildTileContenTable(content, 2);
tile.appendChild(table);
return tile;
}
function buildStreamsTypeTile(responseData) {
const streamsCount = responseData?.type_streams?.doc_count || 0;
const streamsSize = humanFileSize(responseData?.type_streams?.media_size || 0);
const duration = responseData?.type_streams?.duration_str || 'NA';
const content = {
Videos: `${streamsCount}`,
'Media Size': `${streamsSize}`,
Duration: duration,
};
const tile = buildTile('Streams: ');
const table = buildTileContenTable(content, 2);
tile.appendChild(table);
return tile;
}
function buildChannelTile(responseData) {
let tile = buildTile(`Total Channels: ${responseData.channels.total || 0}`);
let message = document.createElement('p');
message.innerHTML = `subscribed: ${responseData.channels.sub_true || 0}`;
tile.appendChild(message);
let tile = buildTile('Channels: ');
const total = responseData.doc_count || 0;
const subscribed = responseData.subscribed_true || 0;
const active = responseData.active_true || 0;
const content = {
Subscribed: subscribed,
Active: active,
Total: total,
};
const table = buildTileContenTable(content, 3);
tile.appendChild(table);
return tile;
}
function buildPlaylistTile(responseData) {
let tile = buildTile(`Total Playlists: ${responseData.playlists.total || 0}`);
let message = document.createElement('p');
message.innerHTML = `subscribed: ${responseData.playlists.sub_true || 0}`;
tile.appendChild(message);
let tile = buildTile('Playlists: ');
const total = responseData.doc_count || 0;
const subscribed = responseData.subscribed_true || 0;
const active = responseData.active_true || 0;
const content = {
Subscribed: subscribed,
Active: active,
Total: total,
};
const table = buildTileContenTable(content, 2);
tile.appendChild(table);
return tile;
}
function buildDownloadTile(responseData) {
let tile = buildTile('Downloads');
let message = document.createElement('p');
message.innerHTML = `
pending: ${responseData.downloads.pending || 0}<br>
ignored: ${responseData.downloads.ignore || 0}<br>
`;
tile.appendChild(message);
const pendingTotal = responseData.pending || 0;
let tile = buildTile(`Downloads Pending: ${pendingTotal}`);
const pendingVideos = responseData.pending_videos || 0;
const pendingShorts = responseData.pending_shorts || 0;
const pendingStreams = responseData.pending_streams || 0;
const content = {
Videos: pendingVideos,
Shorts: pendingShorts,
Streams: pendingStreams,
};
const table = buildTileContenTable(content, 3);
tile.appendChild(table);
return tile;
}
@@ -81,24 +192,38 @@ function watchStats() {
let watchBox = document.getElementById('watchBox');
clearLoading(watchBox);
for (const property in responseData) {
let tile = buildWatchTile(property, responseData[property]);
watchBox.appendChild(tile);
}
let watchedTile = buildWatchTile('watched', responseData.watched);
watchBox.appendChild(watchedTile);
let unwatchedTile = buildWatchTile('unwatched', responseData.unwatched);
watchBox.appendChild(unwatchedTile);
}
function buildWatchTile(title, watchDetail) {
let tile = buildTile(`Total ${title}`);
let message = document.createElement('p');
message.innerHTML = `
${watchDetail.items} Videos<br>
${watchDetail.duration} Seconds<br>
${watchDetail.duration_str} Playback
`;
if (watchDetail.progress) {
message.innerHTML += `<br>${Number(watchDetail.progress * 100).toFixed(2)}%`;
const items = watchDetail?.items ?? 0;
const duration = watchDetail?.duration ?? 0;
const duration_str = watchDetail?.duration_str ?? '0s';
const hasProgess = !!watchDetail?.progress;
const progress = (Number(watchDetail?.progress) * 100).toFixed(2) ?? '0';
let titleCapizalized = capitalizeFirstLetter(title);
if (hasProgess) {
titleCapizalized = `${progress}% ` + titleCapizalized;
}
tile.appendChild(message);
let tile = buildTile(titleCapizalized);
const content = {
Videos: items,
Seconds: duration,
Duration: duration_str,
};
const table = buildTileContenTable(content, 3);
tile.appendChild(table);
return tile;
}
@@ -123,16 +248,20 @@ function downloadHist() {
function buildDailyStat(dailyStat) {
let tile = buildTile(dailyStat.date);
let message = document.createElement('p');
message.innerText = `new videos: ${dailyStat.count}`;
const isExactlyOne = dailyStat.count === 1;
let text = 'Videos';
if (isExactlyOne) {
text = 'Video';
}
message.innerText = `+${dailyStat.count} ${text}
${humanFileSize(dailyStat.media_size)}`;
tile.appendChild(message);
return tile;
}
function humanFileSize(size) {
let i = size === 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));
return (size / Math.pow(1024, i)).toFixed(1) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i];
}
function buildChannelRow(id, name, value) {
let tableRow = document.createElement('tr');
@@ -189,6 +318,81 @@ function addBiggestChannelByMediaSize() {
}
}
function clearLoading(dashBox) {
dashBox.querySelector('#loading').remove();
}
function capitalizeFirstLetter(string) {
// source: https://stackoverflow.com/a/1026087
return string.charAt(0).toUpperCase() + string.slice(1);
}
function humanFileSize(size) {
let i = size === 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));
return (size / Math.pow(1024, i)).toFixed(1) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i];
}
function buildTile(titleText) {
let tile = document.createElement('div');
tile.classList.add('info-box-item');
let title = document.createElement('h3');
title.innerText = titleText;
tile.appendChild(title);
return tile;
}
function buildTileContenTable(content, rowsWanted) {
let contentEntries = Object.entries(content);
const nbsp = '\u00A0'; // No-Break Space https://www.compart.com/en/unicode/U+00A0
// Do not add spacing rows when on mobile device
const isMobile = window.matchMedia('(max-width: 600px)');
if (!isMobile.matches) {
if (contentEntries.length < rowsWanted) {
const rowsToAdd = rowsWanted - contentEntries.length;
for (let i = 0; i < rowsToAdd; i++) {
contentEntries.push([nbsp, nbsp]);
}
}
}
const table = document.createElement('table');
table.classList.add('agg-channel-table');
const tableBody = document.createElement('tbody');
for (const [key, value] of contentEntries) {
const row = document.createElement('tr');
const leftCell = document.createElement('td');
leftCell.classList.add('agg-channel-name');
// Do not add ":" when its a spacing entry
const keyText = key === nbsp ? key : `${key}: `;
const leftText = document.createTextNode(keyText);
leftCell.appendChild(leftText);
const rightCell = document.createElement('td');
rightCell.classList.add('agg-channel-right-align');
const rightText = document.createTextNode(value);
rightCell.appendChild(rightText);
row.appendChild(leftCell);
row.appendChild(rightCell);
tableBody.appendChild(row);
}
table.appendChild(tableBody);
return table;
}
function biggestChannel() {
addBiggestChannelByDocCount();
addBiggestChannelByDuration();
@@ -197,6 +401,7 @@ function biggestChannel() {
async function buildStats() {
primaryStats();
secondaryStats();
watchStats();
downloadHist();
biggestChannel();