diff --git a/backend/channel/serializers.py b/backend/channel/serializers.py
index d2dc4184..22db7565 100644
--- a/backend/channel/serializers.py
+++ b/backend/channel/serializers.py
@@ -45,6 +45,18 @@ class ChannelSerializer(serializers.Serializer):
channel_overwrites = ChannelOverwriteSerializer(required=False)
channel_subs = serializers.IntegerField()
channel_subscribed = serializers.BooleanField()
+ channel_video_count = serializers.IntegerField(
+ required=False, allow_null=True
+ )
+ channel_video_duration = serializers.IntegerField(
+ required=False, allow_null=True
+ )
+ channel_video_duration_str = serializers.CharField(
+ required=False, allow_null=True
+ )
+ channel_video_media_size = serializers.IntegerField(
+ required=False, allow_null=True
+ )
channel_tags = serializers.ListField(
child=serializers.CharField(), required=False
)
diff --git a/backend/channel/views.py b/backend/channel/views.py
index 204aaf73..c9da6259 100644
--- a/backend/channel/views.py
+++ b/backend/channel/views.py
@@ -12,6 +12,8 @@ from channel.serializers import (
from channel.src.index import YoutubeChannel, channel_overwrites
from channel.src.nav import ChannelNav
from common.serializers import ErrorResponseSerializer
+from common.src.es_connect import ElasticWrap
+from common.src.helper import get_duration_str
from common.src.urlparser import Parser
from common.views_base import AdminWriteOnly, ApiBaseView
from drf_spectacular.utils import (
@@ -24,6 +26,48 @@ from task.tasks import index_channel_playlists, subscribe_to
class ChannelApiListView(ApiBaseView):
+ @staticmethod
+ def _get_channel_video_stats(channel_ids):
+ if not channel_ids:
+ return {}
+
+ data = {
+ "size": 0,
+ "query": {"terms": {"channel.channel_id": channel_ids}},
+ "aggs": {
+ "channel_stats": {
+ "multi_terms": {
+ "size": len(channel_ids),
+ "terms": [
+ {"field": "channel.channel_id"},
+ {"field": "channel.channel_name.keyword"},
+ ],
+ },
+ "aggs": {
+ "doc_count": {"value_count": {"field": "_index"}},
+ "duration": {"sum": {"field": "player.duration"}},
+ "media_size": {"sum": {"field": "media_size"}},
+ },
+ }
+ },
+ }
+ response, _ = ElasticWrap("ta_video/_search").get(data=data)
+ buckets = (
+ response.get("aggregations", {})
+ .get("channel_stats", {})
+ .get("buckets", [])
+ )
+ stats = {}
+ for bucket in buckets:
+ duration_value = int(bucket["duration"]["value"])
+ stats[bucket["key"][0]] = {
+ "channel_video_count": int(bucket["doc_count"]["value"]),
+ "channel_video_duration": duration_value,
+ "channel_video_duration_str": get_duration_str(duration_value),
+ "channel_video_media_size": int(bucket["media_size"]["value"]),
+ }
+ return stats
+
"""resolves to /api/channel/
GET: returns list of channels
POST: edit a list of channels
@@ -59,6 +103,23 @@ class ChannelApiListView(ApiBaseView):
self.data["query"] = {"bool": {"must": must_list}}
self.get_document_list(request)
+ if self.response.get("data"):
+ channel_ids = [
+ channel["channel_id"] for channel in self.response["data"]
+ ]
+ channel_stats = self._get_channel_video_stats(channel_ids)
+ for channel in self.response["data"]:
+ channel.update(
+ channel_stats.get(
+ channel["channel_id"],
+ {
+ "channel_video_count": 0,
+ "channel_video_duration": 0,
+ "channel_video_duration_str": get_duration_str(0),
+ "channel_video_media_size": 0,
+ },
+ )
+ )
serializer = ChannelListSerializer(self.response)
return Response(serializer.data)
diff --git a/backend/user/serializers.py b/backend/user/serializers.py
index 4a54aa4c..494462ad 100644
--- a/backend/user/serializers.py
+++ b/backend/user/serializers.py
@@ -34,9 +34,15 @@ class UserMeConfigSerializer(serializers.Serializer):
view_style_home = serializers.ChoiceField(
choices=["grid", "list", "table"]
)
- view_style_channel = serializers.ChoiceField(choices=["grid", "list"])
- view_style_downloads = serializers.ChoiceField(choices=["grid", "list"])
- view_style_playlist = serializers.ChoiceField(choices=["grid", "list"])
+ view_style_channel = serializers.ChoiceField(
+ choices=["grid", "list", "table"]
+ )
+ view_style_downloads = serializers.ChoiceField(
+ choices=["grid", "list", "table"]
+ )
+ view_style_playlist = serializers.ChoiceField(
+ choices=["grid", "list", "table"]
+ )
vid_type_filter = serializers.ChoiceField(
choices=VideoTypeEnum.values_known(), allow_null=True
)
diff --git a/frontend/src/components/ChannelList.tsx b/frontend/src/components/ChannelList.tsx
index 7df96c8e..e94981c3 100644
--- a/frontend/src/components/ChannelList.tsx
+++ b/frontend/src/components/ChannelList.tsx
@@ -3,12 +3,15 @@ import { ChannelType } from '../pages/Channels';
import Routes from '../configuration/routes/RouteList';
import updateChannelSubscription from '../api/actions/updateChannelSubscription';
import formatDate from '../functions/formatDates';
+import humanFileSize from '../functions/humanFileSize';
+import { FileSizeUnits } from '../api/actions/updateUserConfig';
import FormattedNumber from './FormattedNumber';
import Button from './Button';
import ChannelIcon from './ChannelIcon';
import ChannelBanner from './ChannelBanner';
import LoadingIndicator from './LoadingIndicator';
import { useUserConfigStore } from '../stores/UserConfigStore';
+import { ViewStylesEnum } from '../configuration/constants/ViewStyle';
type ChannelListProps = {
channelList: ChannelType[] | undefined;
@@ -18,6 +21,7 @@ type ChannelListProps = {
const ChannelList = ({ channelList, refreshChannelList }: ChannelListProps) => {
const { userConfig } = useUserConfigStore();
const viewStyle = userConfig.view_style_channel;
+ const useSiUnits = userConfig.file_size_unit === FileSizeUnits.Metric;
if (!channelList) {
return
| Channel | +Subscribers | +Videos | +Duration | +Media size | +Last refreshed | +Status | +
|---|---|---|---|---|---|---|
|
+
+
+
+
+
+
+ {channel.channel_name}
+
+ |
+
+ {channel.channel_subs !== null ? (
+ |
+ {channel.channel_video_count ?? 0} | +{channel.channel_video_duration_str ?? '0s'} | +{humanFileSize(channel.channel_video_media_size ?? 0, useSiUnits)} | +{formatDate(channel.channel_last_refresh)} | ++ {channel.channel_subscribed ? ( + | +