diff --git a/tubearchivist/api/src/aggs.py b/tubearchivist/api/src/aggs.py
index 1e2f54c..ac6c2e6 100644
--- a/tubearchivist/api/src/aggs.py
+++ b/tubearchivist/api/src/aggs.py
@@ -117,7 +117,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"),
diff --git a/tubearchivist/home/src/ta/helper.py b/tubearchivist/home/src/ta/helper.py
index 4143ed6..8bce809 100644
--- a/tubearchivist/home/src/ta/helper.py
+++ b/tubearchivist/home/src/ta/helper.py
@@ -178,7 +178,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)
diff --git a/tubearchivist/static/stats.js b/tubearchivist/static/stats.js
index 2373266..3336753 100644
--- a/tubearchivist/static/stats.js
+++ b/tubearchivist/static/stats.js
@@ -8,13 +8,18 @@ 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);
}
@@ -26,51 +31,133 @@ function clearLoading(dashBox) {
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 buildVideoTile(responseData) {
- let tile = buildTile(`Total Videos: ${responseData.videos.total || 0}`);
- let message = document.createElement('p');
- message.innerHTML = `
- videos: ${responseData.videos.videos || 0}
- shorts: ${responseData.videos.shorts || 0}
- streams: ${responseData.videos.streams || 0}
- `;
- tile.appendChild(message);
+ 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(`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.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(`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.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');
- let message = document.createElement('p');
- message.innerHTML = `
- pending: ${responseData.downloads.pending || 0}
- ignored: ${responseData.downloads.ignore || 0}
- `;
- tile.appendChild(message);
+
+ 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;
}
@@ -81,24 +168,48 @@ function watchStats() {
let watchBox = document.getElementById('watchBox');
clearLoading(watchBox);
- for (const property in responseData) {
- let tile = buildWatchTile(property, responseData[property]);
- watchBox.appendChild(tile);
- }
+ 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) {
- let tile = buildTile(`Total ${title}`);
- let message = document.createElement('p');
- message.innerHTML = `
- ${watchDetail.items} Videos
- ${watchDetail.duration} Seconds
- ${watchDetail.duration_str} Playback
- `;
- if (watchDetail.progress) {
- message.innerHTML += `
${Number(watchDetail.progress * 100).toFixed(2)}%`;
+ const items = watchDetail.items || 0;
+ const duration = watchDetail.duration || 0;
+ const duration_str = watchDetail.duration_str || 0;
+ const hasProgess = !!watchDetail.progress;
+ const progress = Number(watchDetail.progress * 100).toFixed(2);
+
+ let titleCapizalized = capitalizeFirstLetter(title);
+
+ if (hasProgess) {
+ titleCapizalized = `${progress}% ` + titleCapizalized;
}
- tile.appendChild(message);
+
+ let tile = buildTile(titleCapizalized);
+
+ const content = {
+ Videos: items,
+ Seconds: duration,
+ Playback: duration_str,
+ };
+
+ const table = buildTileContenTable(content, 3);
+
+ tile.appendChild(table);
+
return tile;
}
@@ -123,7 +234,15 @@ 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}`;
+
tile.appendChild(message);
return tile;
}