Add table for channels
This commit is contained in:
parent
c4ac6441bd
commit
d8afde8ef4
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 <LoadingIndicator />;
|
||||
|
|
@ -26,6 +30,84 @@ const ChannelList = ({ channelList, refreshChannelList }: ChannelListProps) => {
|
|||
return <h2>No channels found...</h2>;
|
||||
}
|
||||
|
||||
if (viewStyle === ViewStylesEnum.Table) {
|
||||
return (
|
||||
<div className={`channel-item ${viewStyle}`}>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Channel</th>
|
||||
<th>Subscribers</th>
|
||||
<th>Videos</th>
|
||||
<th>Duration</th>
|
||||
<th>Media size</th>
|
||||
<th>Last refreshed</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{channelList.map(channel => {
|
||||
return (
|
||||
<tr key={channel.channel_id}>
|
||||
<td className="no-nowrap">
|
||||
<div className="channel-table-title">
|
||||
<div className="round-img">
|
||||
<Link to={Routes.Channel(channel.channel_id)}>
|
||||
<ChannelIcon
|
||||
channelId={channel.channel_id}
|
||||
channelThumbUrl={channel.channel_thumb_url}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
<Link to={Routes.Channel(channel.channel_id)}>{channel.channel_name}</Link>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{channel.channel_subs !== null ? (
|
||||
<FormattedNumber text="" number={channel.channel_subs} />
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</td>
|
||||
<td>{channel.channel_video_count ?? 0}</td>
|
||||
<td>{channel.channel_video_duration_str ?? '0s'}</td>
|
||||
<td>{humanFileSize(channel.channel_video_media_size ?? 0, useSiUnits)}</td>
|
||||
<td>{formatDate(channel.channel_last_refresh)}</td>
|
||||
<td>
|
||||
{channel.channel_subscribed ? (
|
||||
<Button
|
||||
label="Unsubscribe"
|
||||
className="unsubscribe"
|
||||
type="button"
|
||||
title={`Unsubscribe from ${channel.channel_name}`}
|
||||
onClick={async () => {
|
||||
await updateChannelSubscription(channel.channel_id, false);
|
||||
refreshChannelList(true);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
label="Subscribe"
|
||||
type="button"
|
||||
title={`Subscribe to ${channel.channel_name}`}
|
||||
onClick={async () => {
|
||||
await updateChannelSubscription(channel.channel_id, true);
|
||||
refreshChannelList(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{channelList.map(channel => {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { useOutletContext } from 'react-router-dom';
|
|||
import loadChannelList, { ChannelsListResponse } from '../api/loader/loadChannelList';
|
||||
import iconGridView from '/img/icon-gridview.svg';
|
||||
import iconListView from '/img/icon-listview.svg';
|
||||
import iconTableView from '/img/icon-tableview.svg';
|
||||
import iconAdd from '/img/icon-add.svg';
|
||||
import iconFilter from '/img/icon-filter.svg';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
|
@ -41,6 +42,10 @@ export type ChannelType = {
|
|||
channel_tags?: string[];
|
||||
channel_thumb_url: string;
|
||||
channel_tvart_url: string;
|
||||
channel_video_count?: number;
|
||||
channel_video_duration?: number;
|
||||
channel_video_duration_str?: string;
|
||||
channel_video_media_size?: number;
|
||||
};
|
||||
|
||||
const Channels = () => {
|
||||
|
|
@ -194,6 +199,17 @@ const Channels = () => {
|
|||
data-value="list"
|
||||
alt="list view"
|
||||
/>
|
||||
<img
|
||||
src={iconTableView}
|
||||
onClick={() => {
|
||||
handleUserConfigUpdate({
|
||||
view_style_channel: ViewStylesEnum.Table as ViewStylesType,
|
||||
});
|
||||
}}
|
||||
data-origin="channel"
|
||||
data-value="table"
|
||||
alt="table view"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1078,10 +1078,60 @@ video:-webkit-full-screen {
|
|||
gap: 1rem;
|
||||
}
|
||||
|
||||
.channel-list.table {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.channel-item.list {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.channel-item.table {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.channel-item.table table {
|
||||
min-width: 700px;
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.channel-item.table th,
|
||||
.channel-item.table td {
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.channel-item.table th {
|
||||
font-weight: 600;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.channel-item.table td {
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.channel-item.table td.no-nowrap {
|
||||
white-space: normal;
|
||||
word-break: keep-all;
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
.channel-item.table td img,
|
||||
.channel-item.table th img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.channel-table-title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.channel-item.grid > .info-box {
|
||||
display: block;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue