Add video reference tracking and display
- Add "Most referenced" sort option to sort by backlink count - Backend now supports sorting by referenced_by_count field - Search results now display reference counts as badges: - Shows number of backlinks (videos linking to this one) - Shows number of internal references (outbound links) - Reference badges appear alongside transcript source badges 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -286,6 +286,24 @@ def parse_channel_params(values: Iterable[Optional[str]]) -> List[str]:
|
||||
return channels
|
||||
|
||||
|
||||
def build_year_filter(year: Optional[str]) -> Optional[Dict]:
|
||||
if not year:
|
||||
return None
|
||||
try:
|
||||
year_int = int(year)
|
||||
return {
|
||||
"range": {
|
||||
"date": {
|
||||
"gte": f"{year_int}-01-01",
|
||||
"lt": f"{year_int + 1}-01-01",
|
||||
"format": "yyyy-MM-dd"
|
||||
}
|
||||
}
|
||||
}
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
def build_channel_filter(channels: Optional[Sequence[str]]) -> Optional[Dict]:
|
||||
if not channels:
|
||||
return None
|
||||
@@ -320,6 +338,7 @@ def build_query_payload(
|
||||
query: str,
|
||||
*,
|
||||
channels: Optional[Sequence[str]] = None,
|
||||
year: Optional[str] = None,
|
||||
sort: str = "relevant",
|
||||
use_exact: bool = True,
|
||||
use_fuzzy: bool = True,
|
||||
@@ -333,6 +352,10 @@ def build_query_payload(
|
||||
if channel_filter:
|
||||
filters.append(channel_filter)
|
||||
|
||||
year_filter = build_year_filter(year)
|
||||
if year_filter:
|
||||
filters.append(year_filter)
|
||||
|
||||
if use_query_string:
|
||||
base_fields = ["title^3", "description^2", "transcript_full", "transcript_secondary_full"]
|
||||
qs_query = (query or "").strip() or "*"
|
||||
@@ -376,6 +399,8 @@ def build_query_payload(
|
||||
body["sort"] = [{"date": {"order": "desc"}}]
|
||||
elif sort == "older":
|
||||
body["sort"] = [{"date": {"order": "asc"}}]
|
||||
elif sort == "referenced":
|
||||
body["sort"] = [{"referenced_by_count": {"order": "desc"}}]
|
||||
return body
|
||||
|
||||
if query:
|
||||
@@ -479,6 +504,8 @@ def build_query_payload(
|
||||
body["sort"] = [{"date": {"order": "desc"}}]
|
||||
elif sort == "older":
|
||||
body["sort"] = [{"date": {"order": "asc"}}]
|
||||
elif sort == "referenced":
|
||||
body["sort"] = [{"referenced_by_count": {"order": "desc"}}]
|
||||
return body
|
||||
|
||||
|
||||
@@ -570,6 +597,53 @@ def create_app(config: AppConfig = CONFIG) -> Flask:
|
||||
data.sort(key=lambda item: item["Name"].lower())
|
||||
return jsonify(data)
|
||||
|
||||
@app.route("/api/years")
|
||||
def years():
|
||||
body = {
|
||||
"size": 0,
|
||||
"aggs": {
|
||||
"years": {
|
||||
"date_histogram": {
|
||||
"field": "date",
|
||||
"calendar_interval": "year",
|
||||
"format": "yyyy",
|
||||
"order": {"_key": "desc"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if config.elastic.debug:
|
||||
LOGGER.info(
|
||||
"Elasticsearch years request: %s",
|
||||
json.dumps({"index": index, "body": body}, indent=2),
|
||||
)
|
||||
|
||||
response = client.search(index=index, body=body)
|
||||
|
||||
if config.elastic.debug:
|
||||
LOGGER.info(
|
||||
"Elasticsearch years response: %s",
|
||||
json.dumps(response, indent=2, default=str),
|
||||
)
|
||||
|
||||
buckets = (
|
||||
response.get("aggregations", {})
|
||||
.get("years", {})
|
||||
.get("buckets", [])
|
||||
)
|
||||
|
||||
data = [
|
||||
{
|
||||
"Year": bucket.get("key_as_string"),
|
||||
"Count": bucket.get("doc_count", 0),
|
||||
}
|
||||
for bucket in buckets
|
||||
if bucket.get("doc_count", 0) > 0
|
||||
]
|
||||
|
||||
return jsonify(data)
|
||||
|
||||
@app.route("/api/search")
|
||||
def search():
|
||||
query = request.args.get("q", "", type=str)
|
||||
@@ -578,6 +652,7 @@ def create_app(config: AppConfig = CONFIG) -> Flask:
|
||||
if legacy_channel:
|
||||
raw_channels.append(legacy_channel)
|
||||
channels = parse_channel_params(raw_channels)
|
||||
year = request.args.get("year", "", type=str) or None
|
||||
sort = request.args.get("sort", "relevant", type=str)
|
||||
page = max(request.args.get("page", 0, type=int), 0)
|
||||
size = max(request.args.get("size", 10, type=int), 1)
|
||||
@@ -598,6 +673,7 @@ def create_app(config: AppConfig = CONFIG) -> Flask:
|
||||
payload = build_query_payload(
|
||||
query,
|
||||
channels=channels,
|
||||
year=year,
|
||||
sort=sort,
|
||||
use_exact=use_exact,
|
||||
use_fuzzy=use_fuzzy,
|
||||
@@ -671,6 +747,8 @@ def create_app(config: AppConfig = CONFIG) -> Flask:
|
||||
"primary": bool(highlight_map.get("transcript_full")),
|
||||
"secondary": bool(highlight_map.get("transcript_secondary_full")),
|
||||
},
|
||||
"internal_references_count": source.get("internal_references_count", 0),
|
||||
"referenced_by_count": source.get("referenced_by_count", 0),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -716,6 +794,7 @@ def create_app(config: AppConfig = CONFIG) -> Flask:
|
||||
if legacy_channel:
|
||||
raw_channels.append(legacy_channel)
|
||||
channels = parse_channel_params(raw_channels)
|
||||
year = request.args.get("year", "", type=str) or None
|
||||
interval = (request.args.get("interval", "month") or "month").lower()
|
||||
allowed_intervals = {"day", "week", "month", "quarter", "year"}
|
||||
if interval not in allowed_intervals:
|
||||
@@ -727,6 +806,9 @@ def create_app(config: AppConfig = CONFIG) -> Flask:
|
||||
channel_filter = build_channel_filter(channels)
|
||||
if channel_filter:
|
||||
filters.append(channel_filter)
|
||||
year_filter = build_year_filter(year)
|
||||
if year_filter:
|
||||
filters.append(year_filter)
|
||||
if start or end:
|
||||
range_filter: Dict[str, Dict[str, Dict[str, str]]] = {"range": {"date": {}}}
|
||||
if start:
|
||||
|
||||
Reference in New Issue
Block a user