/* Shared YouTube-channel helpers for the video side: detect a pasted channel
* link, render channel/video cards, and follow/unfollow. Used by the search
* page (paste a link → Follow), the wishlist YouTube tab, and the watchlist.
* Scoped under window.VideoYoutube; all CSS is .vyt-* (never touches music wl-*).
*/
(function () {
'use strict';
function esc(s) {
return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
});
}
// A pasted YouTube *channel* reference: a full channel URL or a bare @handle.
// Deliberately NOT bare words, so normal title searches don't trigger it.
function isChannelRef(q) {
q = (q || '').trim();
if (/^@[\w.\-]{2,}$/.test(q)) return true;
if (!/youtube\.com/i.test(q)) return false;
return /youtube\.com\/(@[\w.\-]+|channel\/UC[\w\-]+|c\/[\w.\-]+|user\/[\w.\-]+)/i.test(q);
}
// A pasted YouTube *playlist* link (or bare PL/OL/FL/UU id). Rejects mixes (RD…)
// and personal lists (WL/LL) — same rule as the backend parse_playlist_id.
function isPlaylistRef(q) {
q = (q || '').trim();
var m = /[?&]list=([A-Za-z0-9_-]+)/.exec(q);
var id = m ? m[1] : (/^(PL|OL|FL|UU)[A-Za-z0-9_-]{8,}$/.test(q) ? q : null);
return !!(id && !/^(RD|UL|LL|WL)/.test(id));
}
function fmtDate(iso) {
if (!iso) return '';
var d = new Date(iso.length <= 10 ? iso + 'T00:00:00' : iso);
if (isNaN(d)) return '';
return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
}
function initials(s) {
var w = String(s || '').trim().split(/\s+/).filter(Boolean);
return (w.slice(0, 2).map(function (x) { return x[0]; }).join('') || '▶').toUpperCase();
}
// Route YouTube CDN images through our same-origin proxy so hotlink/CORS
// policy can't blank them out. Non-YouTube / already-proxied urls pass through.
function img(url) {
if (!url) return url;
if (url.indexOf('//') === 0) url = 'https:' + url; // protocol-relative
if (/^https:\/\/([\w-]+\.)*(ytimg\.com|ggpht\.com|googleusercontent\.com)\//i.test(url))
return '/api/video/img?u=' + encodeURIComponent(url);
return url;
}
function avatar(ch, cls) {
var url = ch && (ch.poster_url || ch.avatar_url);
var ini = esc(initials(ch && ch.title));
if (url) {
// If the image can't load, swap to the initials chip (never an empty circle).
return '' + ini + '\'">';
}
return '' + ini + '';
}
// A single wished video tile (thumbnail, title, date, remove).
function videoCard(v) {
var thumb = v.still_url
? ''
: '';
var date = fmtDate(v.published_at);
return '
';
}
function followBtn(following) {
return '';
}
// A pasted-playlist result chip (mirrors searchCard): cover, title, owner +
// count, and an Add-to-watchlist toggle (data-vyt-follow-playlist).
function playlistCard(pl, following) {
var cover = pl.thumbnail_url
? '▤\'">'
: '▤';
var sub = [];
if (pl.channel_title) sub.push(esc(pl.channel_title));
if (pl.video_count != null) sub.push(esc(pl.video_count) + ' videos');
return '