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
This commit is contained in:
Simon 2023-11-19 22:02:23 +07:00
commit 1315e836a4
No known key found for this signature in database
GPG Key ID: 2C15AA5E89985DD4
17 changed files with 626 additions and 343 deletions

View File

@ -38,6 +38,6 @@ body:
attributes: attributes:
label: Relevant log output 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. 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: validations:
required: true required: true

View File

@ -24,62 +24,159 @@ class AggBase:
raise NotImplementedError raise NotImplementedError
class Primary(AggBase): class Video(AggBase):
"""primary aggregation for total documents indexed""" """get video stats"""
name = "primary" name = "video_stats"
path = "ta_video,ta_channel,ta_playlist,ta_subtitle,ta_download/_search" path = "ta_video/_search"
data = { data = {
"size": 0, "size": 0,
"aggs": { "aggs": {
"video_type": { "video_type": {
"filter": {"exists": {"field": "active"}}, "terms": {"field": "vid_type"},
"aggs": {"filtered": {"terms": {"field": "vid_type"}}}, "aggs": {
"media_size": {"sum": {"field": "media_size"}},
"duration": {"sum": {"field": "player.duration"}},
},
}, },
"channel_total": {"value_count": {"field": "channel_active"}}, "video_active": {
"channel_sub": {"terms": {"field": "channel_subscribed"}}, "terms": {"field": "active"},
"playlist_total": {"value_count": {"field": "playlist_active"}}, "aggs": {
"playlist_sub": {"terms": {"field": "playlist_subscribed"}}, "media_size": {"sum": {"field": "media_size"}},
"download": {"terms": {"field": "status"}}, "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): def process(self):
"""make the call""" """process aggregation"""
aggregations = self.get() aggregations = self.get()
videos = {"total": aggregations["video_type"].get("doc_count")} duration = int(aggregations["duration"]["value"])
videos.update( response = {
{ "doc_count": aggregations["video_count"]["value"],
i.get("key"): i.get("doc_count") "media_size": int(aggregations["video_media_size"]["value"]),
for i in aggregations["video_type"]["filtered"]["buckets"] "duration": duration,
} "duration_str": get_duration_str(duration),
)
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"]
} }
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 = { response = {
"videos": videos, "doc_count": aggregations["channel_count"].get("value"),
"channels": channels,
"playlists": playlists,
"downloads": downloads,
} }
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 return response

View File

@ -152,9 +152,24 @@ urlpatterns = [
name="api-notification", name="api-notification",
), ),
path( path(
"stats/primary/", "stats/video/",
views.StatPrimaryView.as_view(), views.StatVideoView.as_view(),
name="api-stats-primary", 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( path(
"stats/watch/", "stats/watch/",

View File

@ -1,6 +1,14 @@
"""all API views""" """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 api.src.search_processor import SearchProcess
from home.src.download.queue import PendingInteract from home.src.download.queue import PendingInteract
from home.src.download.subscriptions import ( from home.src.download.subscriptions import (
@ -1141,16 +1149,52 @@ class NotificationView(ApiBaseView):
return Response(RedisArchivist().list_items(query)) return Response(RedisArchivist().list_items(query))
class StatPrimaryView(ApiBaseView): class StatVideoView(ApiBaseView):
"""resolves to /api/stats/primary/ """resolves to /api/stats/video/
GET: return document count GET: return video stats
""" """
def get(self, request): def get(self, request):
"""get stats""" """get stats"""
# pylint: disable=unused-argument # 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): class StatWatchProgress(ApiBaseView):

View File

@ -74,7 +74,7 @@
"type": "boolean" "type": "boolean"
}, },
"integrate_sponsorblock": { "integrate_sponsorblock": {
"type" : "boolean" "type": "boolean"
} }
} }
} }
@ -168,7 +168,7 @@
"type": "boolean" "type": "boolean"
}, },
"integrate_sponsorblock": { "integrate_sponsorblock": {
"type" : "boolean" "type": "boolean"
} }
} }
} }
@ -236,19 +236,37 @@
"comment_count": { "comment_count": {
"type": "long" "type": "long"
}, },
"stats" : { "stats": {
"properties" : { "properties": {
"average_rating" : { "average_rating": {
"type" : "float" "type": "float"
}, },
"dislike_count" : { "dislike_count": {
"type" : "long" "type": "long"
}, },
"like_count" : { "like_count": {
"type" : "long" "type": "long"
}, },
"view_count" : { "view_count": {
"type" : "long" "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": { "is_enabled": {
"type": "boolean" "type": "boolean"
}, },
"segments" : { "segments": {
"properties" : { "properties": {
"UUID" : { "UUID": {
"type": "keyword" "type": "keyword"
}, },
"actionType" : { "actionType": {
"type": "keyword" "type": "keyword"
}, },
"category" : { "category": {
"type": "keyword" "type": "keyword"
}, },
"locked" : { "locked": {
"type" : "short" "type": "short"
}, },
"segment" : { "segment": {
"type" : "float" "type": "float"
}, },
"videoDuration" : { "videoDuration": {
"type" : "float" "type": "float"
}, },
"votes" : { "votes": {
"type" : "long" "type": "long"
} }
} }
} }
@ -516,7 +534,7 @@
"format": "epoch_second" "format": "epoch_second"
}, },
"subtitle_index": { "subtitle_index": {
"type" : "long" "type": "long"
}, },
"subtitle_lang": { "subtitle_lang": {
"type": "keyword" "type": "keyword"
@ -525,7 +543,7 @@
"type": "keyword" "type": "keyword"
}, },
"subtitle_line": { "subtitle_line": {
"type" : "text", "type": "text",
"analyzer": "english" "analyzer": "english"
} }
}, },
@ -560,14 +578,14 @@
"type": "keyword" "type": "keyword"
}, },
"comment_text": { "comment_text": {
"type" : "text" "type": "text"
}, },
"comment_timestamp": { "comment_timestamp": {
"type": "date", "type": "date",
"format": "epoch_second" "format": "epoch_second"
}, },
"comment_time_text": { "comment_time_text": {
"type" : "text" "type": "text"
}, },
"comment_likecount": { "comment_likecount": {
"type": "long" "type": "long"
@ -613,4 +631,4 @@
} }
} }
] ]
} }

View File

@ -77,7 +77,7 @@ class Comments:
def get_yt_comments(self): def get_yt_comments(self):
"""get comments from youtube""" """get comments from youtube"""
yt_obs = self.build_yt_obs() 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: if not info_json:
return False, False return False, False

View File

@ -105,11 +105,13 @@ class ReindexPopulate(ReindexBase):
"""get total hits from index""" """get total hits from index"""
index_name = reindex_config["index_name"] index_name = reindex_config["index_name"]
active_key = reindex_config["active_key"] active_key = reindex_config["active_key"]
path = f"{index_name}/_search?filter_path=hits.total" data = {
data = {"query": {"match": {active_key: True}}} "query": {"term": {active_key: {"value": True}}},
response, _ = ElasticWrap(path).post(data=data) "_source": False,
total_hits = response["hits"]["total"]["value"] }
return total_hits total = IndexPaginate(index_name, data, keep_source=True).get_results()
return len(total)
def _get_daily_should(self, total_hits): def _get_daily_should(self, total_hits):
"""calc how many should reindex daily""" """calc how many should reindex daily"""
@ -123,7 +125,7 @@ class ReindexPopulate(ReindexBase):
"""get outdated from index_name""" """get outdated from index_name"""
index_name = reindex_config["index_name"] index_name = reindex_config["index_name"]
refresh_key = reindex_config["refresh_key"] 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 = [ must_list = [
{"match": {reindex_config["active_key"]: True}}, {"match": {reindex_config["active_key"]: True}},
{"range": {refresh_key: {"lte": now_lte}}}, {"range": {refresh_key: {"lte": now_lte}}},

View File

@ -188,11 +188,11 @@ class YoutubeVideo(YouTubeItem, YoutubeSubtitle):
# build json_data basics # build json_data basics
self.json_data = { self.json_data = {
"title": self.youtube_meta["title"], "title": self.youtube_meta["title"],
"description": self.youtube_meta["description"], "description": self.youtube_meta.get("description", ""),
"category": self.youtube_meta["categories"], "category": self.youtube_meta.get("categories", []),
"vid_thumb_url": self.youtube_meta["thumbnail"], "vid_thumb_url": self.youtube_meta["thumbnail"],
"vid_thumb_base64": base64_blur, "vid_thumb_base64": base64_blur,
"tags": self.youtube_meta["tags"], "tags": self.youtube_meta.get("tags", []),
"published": published, "published": published,
"vid_last_refresh": last_refresh, "vid_last_refresh": last_refresh,
"date_downloaded": last_refresh, "date_downloaded": last_refresh,
@ -210,20 +210,13 @@ class YoutubeVideo(YouTubeItem, YoutubeSubtitle):
def _add_stats(self): def _add_stats(self):
"""add stats dicst to json_data""" """add stats dicst to json_data"""
# likes stats = {
like_count = self.youtube_meta.get("like_count", 0) "view_count": self.youtube_meta.get("view_count", 0),
dislike_count = self.youtube_meta.get("dislike_count", 0) "like_count": self.youtube_meta.get("like_count", 0),
average_rating = self.youtube_meta.get("average_rating", 0) "dislike_count": self.youtube_meta.get("dislike_count", 0),
self.json_data.update( "average_rating": self.youtube_meta.get("average_rating", 0),
{ }
"stats": { self.json_data.update({"stats": stats})
"view_count": self.youtube_meta["view_count"],
"like_count": like_count,
"dislike_count": dislike_count,
"average_rating": average_rating,
}
}
)
def build_dl_cache_path(self): def build_dl_cache_path(self):
"""find video path in dl cache""" """find video path in dl cache"""

View File

@ -50,7 +50,14 @@ class UserConfig:
VALID_STYLESHEETS = get_stylesheets() VALID_STYLESHEETS = get_stylesheets()
VALID_VIEW_STYLE = ["grid", "list"] VALID_VIEW_STYLE = ["grid", "list"]
VALID_SORT_ORDER = ["asc", "desc"] VALID_SORT_ORDER = ["asc", "desc"]
VALID_SORT_BY = ["published", "downloaded", "views", "likes"] VALID_SORT_BY = [
"published",
"downloaded",
"views",
"likes",
"duration",
"filesize",
]
VALID_GRID_ITEMS = range(3, 8) VALID_GRID_ITEMS = range(3, 8)
def __init__(self, user_id: str): def __init__(self, user_id: str):

View File

@ -82,6 +82,8 @@
<option value="downloaded" {% if sort_by == "downloaded" %}selected{% endif %}>date downloaded</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="views" {% if sort_by == "views" %}selected{% endif %}>views</option>
<option value="likes" {% if sort_by == "likes" %}selected{% endif %}>likes</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>
<select name="sort_order" id="sort-order" onchange="sortChange(this)"> <select name="sort_order" id="sort-order" onchange="sortChange(this)">
<option value="asc" {% if sort_order == "asc" %}selected{% endif %}>asc</option> <option value="asc" {% if sort_order == "asc" %}selected{% endif %}>asc</option>

View File

@ -65,6 +65,8 @@
<option value="downloaded" {% if sort_by == "downloaded" %}selected{% endif %}>date downloaded</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="views" {% if sort_by == "views" %}selected{% endif %}>views</option>
<option value="likes" {% if sort_by == "likes" %}selected{% endif %}>likes</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>
<select name="sort_order" id="sort-order" onchange="sortChange(this)"> <select name="sort_order" id="sort-order" onchange="sortChange(this)">
<option value="asc" {% if sort_order == "asc" %}selected{% endif %}>asc</option> <option value="asc" {% if sort_order == "asc" %}selected{% endif %}>asc</option>

View File

@ -5,14 +5,26 @@
<h1>Your Archive</h1> <h1>Your Archive</h1>
</div> </div>
<div class="settings-item"> <div class="settings-item">
<h2>Main overview</h2> <h2>Overview</h2>
<div id="primaryBox" class="info-box info-box-4"> <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> <p id="loading">Loading...</p>
</div> </div>
</div> </div>
<div class="settings-item"> <div class="settings-item">
<h2>Watch Progress</h2> <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> <p id="loading">Loading...</p>
</div> </div>
</div> </div>

View File

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

View File

@ -111,6 +111,8 @@ class ArchivistResultsView(ArchivistViewConfig):
"likes": "stats.like_count", "likes": "stats.like_count",
"downloaded": "date_downloaded", "downloaded": "date_downloaded",
"published": "published", "published": "published",
"duration": "player.duration",
"filesize": "media_size",
} }
sort_by = sort_by_map[self.context["sort_by"]] sort_by = sort_by_map[self.context["sort_by"]]

View File

@ -1,8 +1,8 @@
apprise==1.6.0 apprise==1.6.0
celery==5.3.4 celery==5.3.5
Django==4.2.7 Django==4.2.7
django-auth-ldap==4.6.0 django-auth-ldap==4.6.0
django-cors-headers==4.3.0 django-cors-headers==4.3.1
djangorestframework==3.14.0 djangorestframework==3.14.0
Pillow==10.1.0 Pillow==10.1.0
redis==5.0.1 redis==5.0.1
@ -10,4 +10,4 @@ requests==2.31.0
ryd-client==0.0.6 ryd-client==0.0.6
uWSGI==2.0.23 uWSGI==2.0.23
whitenoise==6.6.0 whitenoise==6.6.0
yt-dlp==2023.10.13 yt-dlp==2023.11.16

View File

@ -180,6 +180,10 @@ button:hover {
text-decoration: underline; text-decoration: underline;
} }
.footer .boxed-content {
text-align: center;
}
/* toggle on-off */ /* toggle on-off */
.toggle { .toggle {
display: flex; display: flex;

View File

@ -5,29 +5,333 @@
/* globals apiRequest */ /* globals apiRequest */
function primaryStats() { function primaryStats() {
let apiEndpoint = '/api/stats/primary/'; 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 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 buildTotalVideoTile(responseData) {
const totalCount = responseData.doc_count || 0;
const totalSize = humanFileSize(responseData.media_size || 0);
const content = {
Items: `${totalCount}`,
'Media Size': `${totalSize}`,
Duration: responseData.duration_str,
};
const tile = buildTile('All: ');
const table = buildTileContenTable(content, 2);
tile.appendChild(table);
return tile;
}
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 = {
Items: `${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 = {
Items: `${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 = {
Items: `${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 = {
Items: `${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 = {
Items: `${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('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('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) {
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;
}
function watchStats() {
let apiEndpoint = '/api/stats/watch/';
let responseData = apiRequest(apiEndpoint, 'GET'); let responseData = apiRequest(apiEndpoint, 'GET');
let primaryBox = document.getElementById('primaryBox'); let watchBox = document.getElementById('watchBox');
clearLoading(watchBox);
clearLoading(primaryBox); let watchedTile = buildWatchTile('watched', responseData.watched);
watchBox.appendChild(watchedTile);
let videoTile = buildVideoTile(responseData); let unwatchedTile = buildWatchTile('unwatched', responseData.unwatched);
primaryBox.appendChild(videoTile); watchBox.appendChild(unwatchedTile);
}
let channelTile = buildChannelTile(responseData); function buildWatchTile(title, watchDetail) {
primaryBox.appendChild(channelTile); 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 playlistTile = buildPlaylistTile(responseData); let titleCapizalized = capitalizeFirstLetter(title);
primaryBox.appendChild(playlistTile);
let downloadTile = buildDownloadTile(responseData); if (hasProgess) {
primaryBox.appendChild(downloadTile); titleCapizalized = `${progress}% ` + titleCapizalized;
}
let tile = buildTile(titleCapizalized);
const content = {
Videos: items,
Seconds: duration,
Duration: duration_str,
};
const table = buildTileContenTable(content, 3);
tile.appendChild(table);
return tile;
}
function downloadHist() {
let apiEndpoint = '/api/stats/downloadhist/';
let responseData = apiRequest(apiEndpoint, 'GET');
let histBox = document.getElementById('downHistBox');
clearLoading(histBox);
if (responseData.length === 0) {
let tile = buildTile('No recent downloads');
histBox.appendChild(tile);
return;
}
for (let i = 0; i < responseData.length; i++) {
const dailyStat = responseData[i];
let tile = buildDailyStat(dailyStat);
histBox.appendChild(tile);
}
}
function buildDailyStat(dailyStat) {
let tile = buildTile(dailyStat.date);
let message = document.createElement('p');
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 buildChannelRow(id, name, value) {
let tableRow = document.createElement('tr');
tableRow.innerHTML = `
<td class="agg-channel-name"><a href="/channel/${id}/">${name}</a></td>
<td class="agg-channel-right-align">${value}</td>
`;
return tableRow;
}
function addBiggestChannelByDocCount() {
let tBody = document.getElementById('biggestChannelTableVideos');
let apiEndpoint = '/api/stats/biggestchannels/?order=doc_count';
const responseData = apiRequest(apiEndpoint, 'GET');
for (let i = 0; i < responseData.length; i++) {
const { id, name, doc_count } = responseData[i];
let tableRow = buildChannelRow(id, name, doc_count);
tBody.appendChild(tableRow);
}
}
function addBiggestChannelByDuration() {
const tBody = document.getElementById('biggestChannelTableDuration');
let apiEndpoint = '/api/stats/biggestchannels/?order=duration';
const responseData = apiRequest(apiEndpoint, 'GET');
for (let i = 0; i < responseData.length; i++) {
const { id, name, duration_str } = responseData[i];
let tableRow = buildChannelRow(id, name, duration_str);
tBody.appendChild(tableRow);
}
}
function addBiggestChannelByMediaSize() {
let tBody = document.getElementById('biggestChannelTableMediaSize');
let apiEndpoint = '/api/stats/biggestchannels/?order=media_size';
const responseData = apiRequest(apiEndpoint, 'GET');
for (let i = 0; i < responseData.length; i++) {
const { id, name, media_size } = responseData[i];
let tableRow = buildChannelRow(id, name, humanFileSize(media_size));
tBody.appendChild(tableRow);
}
} }
function clearLoading(dashBox) { function clearLoading(dashBox) {
dashBox.querySelector('#loading').remove(); 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) { function buildTile(titleText) {
let tile = document.createElement('div'); let tile = document.createElement('div');
tile.classList.add('info-box-item'); tile.classList.add('info-box-item');
@ -89,226 +393,6 @@ function buildTileContenTable(content, rowsWanted) {
return table; return table;
} }
function buildVideoTile(responseData) {
let tile = buildTile(`Video types: `);
const total = responseData.videos.total || 0;
const videos = responseData.videos.videos || 0;
const shorts = responseData.videos.shorts || 0;
const streams = responseData.videos.streams || 0;
const content = {
Videos: `${videos}/${total}`,
Shorts: `${shorts}/${total}`,
Streams: `${streams}/${total}`,
};
const table = buildTileContenTable(content, 3);
tile.appendChild(table);
return tile;
}
function buildChannelTile(responseData) {
let tile = buildTile(`Channels: `);
const total = responseData.channels.total || 0;
const subscribed = responseData.channels.sub_true || 0;
const content = {
Subscribed: `${subscribed}/${total}`,
};
const table = buildTileContenTable(content, 3);
tile.appendChild(table);
return tile;
}
function buildPlaylistTile(responseData) {
let tile = buildTile(`Playlists:`);
const total = responseData.playlists.total || 0;
const subscribed = responseData.playlists.sub_true || 0;
const content = {
Subscribed: `${subscribed}/${total}`,
};
const table = buildTileContenTable(content, 3);
tile.appendChild(table);
return tile;
}
function buildDownloadTile(responseData) {
let tile = buildTile('Downloads');
const pending = responseData.downloads.pending || 0;
const ignored = responseData.downloads.ignore || 0;
const content = {
Pending: pending,
Ignored: ignored,
};
const table = buildTileContenTable(content, 3);
tile.appendChild(table);
return tile;
}
function watchStats() {
let apiEndpoint = '/api/stats/watch/';
let responseData = apiRequest(apiEndpoint, 'GET');
let watchBox = document.getElementById('watchBox');
clearLoading(watchBox);
const { total, watched, unwatched } = responseData;
let firstCard = buildWatchTile('total', total);
watchBox.appendChild(firstCard);
let secondCard = buildWatchTile('watched', watched);
watchBox.appendChild(secondCard);
let thirdCard = buildWatchTile('unwatched', unwatched);
watchBox.appendChild(thirdCard);
}
function capitalizeFirstLetter(string) {
// source: https://stackoverflow.com/a/1026087
return string.charAt(0).toUpperCase() + string.slice(1);
}
function buildWatchTile(title, watchDetail) {
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;
}
let tile = buildTile(titleCapizalized);
const content = {
Videos: items,
Seconds: duration,
Playback: duration_str,
};
const table = buildTileContenTable(content, 3);
tile.appendChild(table);
return tile;
}
function downloadHist() {
let apiEndpoint = '/api/stats/downloadhist/';
let responseData = apiRequest(apiEndpoint, 'GET');
let histBox = document.getElementById('downHistBox');
clearLoading(histBox);
if (responseData.length === 0) {
let tile = buildTile('No recent downloads');
histBox.appendChild(tile);
return;
}
for (let i = 0; i < responseData.length; i++) {
const dailyStat = responseData[i];
let tile = buildDailyStat(dailyStat);
histBox.appendChild(tile);
}
}
function buildDailyStat(dailyStat) {
let tile = buildTile(dailyStat.date);
let message = document.createElement('p');
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');
tableRow.innerHTML = `
<td class="agg-channel-name"><a href="/channel/${id}/">${name}</a></td>
<td class="agg-channel-right-align">${value}</td>
`;
return tableRow;
}
function addBiggestChannelByDocCount() {
let tBody = document.getElementById('biggestChannelTableVideos');
let apiEndpoint = '/api/stats/biggestchannels/?order=doc_count';
const responseData = apiRequest(apiEndpoint, 'GET');
for (let i = 0; i < responseData.length; i++) {
const { id, name, doc_count } = responseData[i];
let tableRow = buildChannelRow(id, name, doc_count);
tBody.appendChild(tableRow);
}
}
function addBiggestChannelByDuration() {
const tBody = document.getElementById('biggestChannelTableDuration');
let apiEndpoint = '/api/stats/biggestchannels/?order=duration';
const responseData = apiRequest(apiEndpoint, 'GET');
for (let i = 0; i < responseData.length; i++) {
const { id, name, duration_str } = responseData[i];
let tableRow = buildChannelRow(id, name, duration_str);
tBody.appendChild(tableRow);
}
}
function addBiggestChannelByMediaSize() {
let tBody = document.getElementById('biggestChannelTableMediaSize');
let apiEndpoint = '/api/stats/biggestchannels/?order=media_size';
const responseData = apiRequest(apiEndpoint, 'GET');
for (let i = 0; i < responseData.length; i++) {
const { id, name, media_size } = responseData[i];
let tableRow = buildChannelRow(id, name, humanFileSize(media_size));
tBody.appendChild(tableRow);
}
}
function biggestChannel() { function biggestChannel() {
addBiggestChannelByDocCount(); addBiggestChannelByDocCount();
addBiggestChannelByDuration(); addBiggestChannelByDuration();
@ -317,6 +401,7 @@ function biggestChannel() {
async function buildStats() { async function buildStats() {
primaryStats(); primaryStats();
secondaryStats();
watchStats(); watchStats();
downloadHist(); downloadHist();
biggestChannel(); biggestChannel();