Skip to content

Commit

Permalink
Enhance video API to support sorting order and improve HTML structure…
Browse files Browse the repository at this point in the history
… for video listing
  • Loading branch information
Satish Surath committed Feb 4, 2025
1 parent c487de2 commit 59901d7
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 99 deletions.
42 changes: 16 additions & 26 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,66 +191,57 @@ def api_channel_status(task_id):

@app.route('/api/videos/<channel_name>', methods=['GET'])
def api_get_videos(channel_name):
"""
List the videos for a given channel from the database,
plus any SummariesV2 entries that exist for each video.
Applies pagination, sorting, and a title filter.
Query params:
- page (int)
- page_size (int)
- sort_by (str) => "title" or "date"
- filter (str) => partial match on title
"""
page = int(request.args.get('page', 1))
page_size = int(request.args.get('page_size', 5)) # default 5 if not provided
sort_by = request.args.get('sort_by', 'title') # or 'date'
sort_by = request.args.get('sort_by', 'title') # "title" or "date"
sort_order = request.args.get('sort_order', 'asc').lower() # "asc" or "desc"
filter_str = request.args.get('filter', '').strip().lower()

session = SessionLocal()
try:
# 1) Query the videos for this channel
query = (
session.query(Video)
.join(VideoFolder, Video.video_id == VideoFolder.video_id)
.filter(VideoFolder.folder_name == channel_name)
)
# 2) Apply optional title filter

# 1) Apply optional title filter
if filter_str:
# We do a simple case-insensitive "like" matching on Video.title
query = query.filter(Video.title.ilike(f"%{filter_str}%"))

# 3) Sorting
# 2) Sorting
# We use sort_by + sort_order
if sort_by == 'title':
query = query.order_by(Video.title.asc())
if sort_order == 'asc':
query = query.order_by(Video.title.asc())
else:
query = query.order_by(Video.title.desc())
elif sort_by == 'date':
# If you want newest first, do desc. Or if you want oldest first, asc.
query = query.order_by(Video.upload_date.desc())
if sort_order == 'asc':
query = query.order_by(Video.upload_date.asc())
else:
query = query.order_by(Video.upload_date.desc())

# 4) Pagination
# 3) Pagination
total = query.count()
offset = (page - 1) * page_size
video_rows = query.offset(offset).limit(page_size).all()

# 5) Build the JSON response
# 4) Build the JSON response
videos_list = []
for vid in video_rows:
# Retrieve all SummariesV2 for this video
summaries_v2_data = []
for s in vid.summaries_v2:
summaries_v2_data.append({
"id": s.id,
"model_name": s.model_name,
"date_generated": s.date_generated.isoformat() if s.date_generated else None,
# If you want, you could include excerpts from s.concise_summary, etc.
})

videos_list.append({
"video_id": vid.video_id,
"title": vid.title or "Untitled",
"upload_date": vid.upload_date or "UnknownDate",
# Now we store the entire set of v2 summaries for the front-end to handle
"summaries_v2": summaries_v2_data
})

Expand All @@ -260,7 +251,6 @@ def api_get_videos(channel_name):
"page_size": page_size,
"videos": videos_list
})

finally:
session.close()

Expand Down
121 changes: 88 additions & 33 deletions static/js/videos.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
document.addEventListener("DOMContentLoaded", () => {
const filterInput = document.getElementById("filterInput");
const sortSelect = document.getElementById("sortSelect");
const applyFilterBtn = document.getElementById("applyFilterBtn");
const videosList = document.getElementById("videosList");
const prevPageBtn = document.getElementById("prevPageBtn");
Expand All @@ -9,37 +8,97 @@ document.addEventListener("DOMContentLoaded", () => {
const summarizeBtn = document.getElementById("summarizeBtn");
const methodSelect = document.getElementById("methodSelect");
const summaryStatus = document.getElementById("summaryStatus");

// For column header links:
const sortTitleLink = document.getElementById("sortTitleLink");
const sortDateLink = document.getElementById("sortDateLink");

let currentPage = 1;
let pageSize = 50;
let totalVideos = 0;
let currentVideos = [];

// Dynamically load available Ollama models
loadOllamaModels();

// Initial load
loadVideos();
// Track sort column & order.
// Default to sorting by 'title' ascending, for example.
let currentSort = {
by: 'title',
order: 'asc'
};

// ==========================
// Define updateSortIndicators
// ==========================
function updateSortIndicators() {
// Clear out any existing indicators
sortTitleLink.innerText = "Title";
sortDateLink.innerText = "Date";

// Show an arrow on whichever is selected
if (currentSort.by === "title") {
sortTitleLink.innerText += (currentSort.order === "asc") ? " ↑" : " ↓";
} else if (currentSort.by === "date") {
sortDateLink.innerText += (currentSort.order === "asc") ? " ↑" : " ↓";
}
}

// =====================
// EVENT LISTENERS
// =====================
// 1) Filter
applyFilterBtn.addEventListener("click", () => {
currentPage = 1;
loadVideos();
});

// 2) Pagination
prevPageBtn.addEventListener("click", () => {
if (currentPage > 1) {
currentPage--;
loadVideos();
}
});

nextPageBtn.addEventListener("click", () => {
if (currentPage * pageSize < totalVideos) {
currentPage++;
loadVideos();
}
});

// 3) Sorting by clicking column headers
sortTitleLink.addEventListener("click", (e) => {
e.preventDefault();

// If already sorting by 'title', flip the order. Otherwise, set to asc.
if (currentSort.by === 'title') {
currentSort.order = (currentSort.order === 'asc') ? 'desc' : 'asc';
} else {
currentSort.by = 'title';
currentSort.order = 'asc';
}
updateSortIndicators();
currentPage = 1;
loadVideos();

});

sortDateLink.addEventListener("click", (e) => {
e.preventDefault();

// If already sorting by 'date', flip the order. Otherwise, set to desc or asc (your preference).
if (currentSort.by === 'date') {
currentSort.order = (currentSort.order === 'asc') ? 'desc' : 'asc';
} else {
currentSort.by = 'date';
// maybe default to descending for date so newest first:
currentSort.order = 'desc';
}
updateSortIndicators();
currentPage = 1;
loadVideos();

});

// 4) Summarize button
summarizeBtn.addEventListener("click", async () => {
const selected = currentVideos
.filter(v => document.getElementById(`check_${v.video_id}`).checked)
Expand All @@ -58,7 +117,7 @@ document.addEventListener("DOMContentLoaded", () => {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
channel_name: channel_name, // the variable from <script> above
channel_name: channel_name,
video_ids: selected,
model: chosenModel
})
Expand All @@ -75,49 +134,45 @@ document.addEventListener("DOMContentLoaded", () => {
}
});

// Load the Ollama models for the Summarize <select>
loadOllamaModels();

// Initially load the first page of videos
loadVideos();

// =====================
// MAIN FUNCTIONS
// =====================
async function loadVideos() {
const filterVal = filterInput.value;
const sortVal = sortSelect.value;
const url = `/api/videos/${channel_name}?page=${currentPage}&page_size=${pageSize}&sort_by=${sortVal}&filter=${encodeURIComponent(filterVal)}`;
const url = `/api/videos/${channel_name}?page=${currentPage}&page_size=${pageSize}`
+ `&sort_by=${currentSort.by}&sort_order=${currentSort.order}`
+ `&filter=${encodeURIComponent(filterVal)}`;

videosList.innerHTML = "Loading...";
videosList.innerHTML = "<tr><td colspan='5'>Loading...</td></tr>";
try {
const res = await fetch(url);
const data = await res.json();
totalVideos = data.total;
currentVideos = data.videos;
renderVideos(data.videos);
pageInfo.innerText = `Page ${data.page} / ${Math.ceil(data.total / data.page_size)}`;
pageInfo.innerText = `Page ${data.page} of ${Math.ceil(data.total / data.page_size)}`;
} catch (err) {
videosList.innerHTML = `Error loading videos: ${err}`;
videosList.innerHTML = `<tr><td colspan='5'>Error loading videos: ${err}</td></tr>`;
}
}

function renderVideos(videos) {
if (!videos || videos.length === 0) {
videosList.innerHTML = "<p>No videos found.</p>";
videosList.innerHTML = "<tr><td colspan='5'>No videos found.</td></tr>";
return;
}

let html = `
<table>
<thead>
<tr>
<th><input type="checkbox" id="selectAll" class="form-check-input videoCheckbox" />&nbsp;&nbsp;All </th>
<th>Title</th>
<th>Date</th>
<th>Existing Summaries</th>
<th>Transcript</th>
</tr>
</thead>
<tbody>
`;

let html = "";
videos.forEach(v => {
let summariesList = "";
if (v.summaries_v2 && v.summaries_v2.length > 0) {
summariesList = v.summaries_v2.map(s => {
// Provide a link to the new route /summaries_v2/<s.id>
return `
<div>
<a href="/summaries_v2/${s.id}" target="_blank">
Expand Down Expand Up @@ -149,7 +204,6 @@ document.addEventListener("DOMContentLoaded", () => {
`;
});

html += "</tbody></table>";
videosList.innerHTML = html;

// "Select All" logic
Expand All @@ -163,14 +217,14 @@ document.addEventListener("DOMContentLoaded", () => {
});
}

// Dynamically load available Ollama models
// Load Ollama models
async function loadOllamaModels() {
try {
const resp = await fetch("/api/ollama/models");
const data = await resp.json();
methodSelect.innerHTML = ""; // clear existing

data.data.forEach(m => {
(data.data || []).forEach(m => {
const opt = document.createElement("option");
opt.value = m.id;
opt.textContent = m.id;
Expand All @@ -182,4 +236,5 @@ document.addEventListener("DOMContentLoaded", () => {
methodSelect.innerHTML = "<option value='phi4:latest'>phi4:latest</option>";
}
}
});
});

Loading

0 comments on commit 59901d7

Please sign in to comment.