Compare commits

...
6 Commits
Author SHA1 Message Date
yusufipek 5e35cf1074 Merge pull request #3 from yusufipk/claude/release-0-1-4
Bump version to 0.1.4
2026-08-22 15:48:39 +03:00
yusufipek aee1748aba Bump version to 0.1.4 2026-08-22 15:38:19 +03:00
yusufipek 079513d5bf Merge pull request #2 from yusufipk/claude/vanity-handle-resolve
Follow the redirect when a handle resolves to a legacy vanity address
2026-08-22 15:37:12 +03:00
yusufipek 1e1c092055 Follow the redirect when a handle resolves to a legacy vanity address
Channels that still carry an old custom URL do not resolve in one step:
navigation/resolve_url answers @MesutCevik with a urlEndpoint pointing at
youtube.com/mesutcevik, and only that second address carries the browseId.
resolveChannel handled a urlEndpoint only when it already contained /channel/,
so those channels threw "kanal çözülemedi" and none of their cards ever got a
badge, anywhere on the site. It now follows an internal redirect up to three
hops.

Search results put a dozen cards from the same channel on screen at once. The
store read that fronts the handle cache is async, so every one of them missed
the cache and asked for the same resolution separately, which the extra hop
would have doubled. Cards now share the in-flight request, the same way
OBScore.baseline already shares a baseline fetch.
2026-08-22 15:35:21 +03:00
yusufipek 51a83a7e96 Merge pull request #1 from yusufipk/claude/youtube-outlier-badge-bug-d58fe1
Score cards and the watch panel again after the player endpoint closed
2026-08-22 15:27:03 +03:00
yusufipek 3e70662ef8 Score cards and the watch panel again after the player endpoint closed
YouTube now answers the InnerTube `player` endpoint with LOGIN_REQUIRED for
requests that carry no session, and the extension deliberately sends none.
`videoDetails` therefore returned nothing for almost every video, which broke
two things at once: cards on channel pages, where YouTube omits the channel
link because you are already on the channel, and the watch page panel, which
starts every render with that same call.

`next` returns the channel id and the exact view count without a session, so
videoDetails reads from there instead. A live stream reports "watching now" in
the same field, so that text is filtered out rather than read as a view count.

Channel pages no longer need the fallback at all: the cards belong to the
channel in the address bar, so pageChannel() reads it from the URL. That also
drops one request per card.

The panel took the video length from the player response to tell Shorts apart;
`next` does not carry it, so it comes from the page's own player element.

test/check.js probed videoDetails with dQw4w9WgXcQ, which keeps answering even
on the broken endpoint and kept the suite green through all of this. It now
probes a current video from the channel it just listed, and asserts the view
count as well as the channel id.
2026-08-22 15:20:40 +03:00
4 changed files with 122 additions and 43 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "__MSG_extName__",
"version": "0.1.3",
"version": "0.1.4",
"description": "__MSG_extDescription__",
"default_locale": "en",
"browser_specific_settings": {
+46 -10
View File
@@ -91,21 +91,47 @@
/* --- Kanal kimliği --------------------------------------------------- */
/* Kanal sayfasındaki kartlarda kanal bağlantısı hiç bulunmuyor: zaten o
* kanaldasın, YouTube adı kartta tekrar etmiyor. O kartların kanalı sayfanın
* kendi adresidir, video başına ayrı bir istek atmaya gerek yok. */
function pageChannel() {
var path = location.pathname;
var m = /^\/channel\/(UC[\w-]{22})(?:\/|$)/.exec(path);
if (m) return { channelId: m[1] };
m = /^\/(@[\w.\-]+)(?:\/|$)/.exec(path);
if (m) return { handle: m[1] };
/* Eski /c/ ve /user/ adresleri: çözüm için tam adres gerekiyor. */
m = /^\/((?:c|user)\/[^/]+)(?:\/|$)/.exec(path);
if (m) return { handle: location.origin + "/" + m[1] };
return null;
}
/* Handle'dan kanal kimliğine çözüm kalıcıdır (değişmez), o yüzden TTL'siz
* saklanır. İzleme sayfasının yan listesinde kartta kanal bağlantısı hiç
* olmayabiliyor; orada son çare videonun kendi ucundan sorulur. */
function channelIdOf(card, videoId) {
var found = handleOf(card);
if (found && found.channelId) return Promise.resolve(found.channelId);
if (found && found.handle) {
return OBStore.readChannelId(found.handle).then(function (cached) {
* saklanır. Arama sonuçlarında aynı kanaldan onlarca kart aynı anda ekrana
* giriyor ve depo okuması asenkron olduğu için hepsi önbelleği ıskalayıp
* aynı çözümü ayrı ayrı isterdi; devam eden istek paylaşılıyor. */
var resolving = {};
function resolveHandle(handle) {
if (resolving[handle]) return resolving[handle];
var promise = OBStore.readChannelId(handle).then(function (cached) {
if (cached) return cached;
return OBTube.resolveChannel(found.handle).then(function (id) {
OBStore.writeChannelId(found.handle, id);
return OBTube.resolveChannel(handle).then(function (id) {
OBStore.writeChannelId(handle, id);
return id;
});
});
resolving[handle] = promise;
promise.catch(function () {}).then(function () { delete resolving[handle]; });
return promise;
}
/* İzleme sayfasının yan listesinde kartta kanal bağlantısı hiç
* olmayabiliyor; orada son çare videonun kendi ucundan sorulur. */
function channelIdOf(card, videoId) {
var found = handleOf(card) || pageChannel();
if (found && found.channelId) return Promise.resolve(found.channelId);
if (found && found.handle) return resolveHandle(found.handle);
var key = "v:" + videoId;
return OBStore.readChannelId(key).then(function (cached) {
if (cached) return cached;
@@ -166,6 +192,16 @@
return m ? m[1] : null;
}
/* Videonun süresi InnerTube'un next yanıtında yok, sayfanın oynatıcısından
* okunuyor. Okunamazsa normal video sayılır: /watch adresinde açılan bir
* Short nadir, yanlış havuzda puanlamaktansa gecikmeli doğruyu beklemek
* anlamsız olurdu. */
function watchIsShort() {
if (location.pathname.indexOf("/shorts/") === 0) return true;
var v = document.querySelector("#movie_player video");
return !!(v && isFinite(v.duration) && v.duration > 0 && v.duration <= 60);
}
function updatePanel() {
if (!settings || !settings.enabled || !settings.showPanel) { OBPanel.remove(); return; }
var videoId = currentWatchId();
@@ -179,7 +215,7 @@
details = d;
if (!d.channelId) throw new Error("kanal bulunamadı");
OBStore.writeChannelId("v:" + videoId, d.channelId);
var isShort = d.lengthSeconds != null && d.lengthSeconds <= 60;
var isShort = watchIsShort();
return OBScore.baseline(d.channelId, isShort).then(function (blob) {
return { blob: blob, isShort: isShort };
});
+55 -26
View File
@@ -268,45 +268,74 @@ var OBTube = (function () {
return post("browse", { browseId: channelId, params: params }).then(round);
}
/* @handle veya kanal adresini UC... kimliğine çevirir. */
function resolveChannel(raw) {
raw = (raw || "").trim();
if (!raw) return Promise.reject(new Error("boş kanal adresi"));
if (/^UC[\w-]{22}$/.test(raw)) return Promise.resolve(raw);
if (raw.indexOf("/channel/") >= 0) {
var tail = raw.split("/channel/")[1].split("/")[0].split("?")[0];
if (tail.indexOf("UC") === 0) return Promise.resolve(tail);
}
var url = raw;
if (url.indexOf("http") !== 0) {
url = "https://www.youtube.com/" + (url.charAt(0) === "@" ? url : "@" + url.replace(/^\/+/, ""));
function channelIdIn(url) {
if (!url || url.indexOf("/channel/") < 0) return null;
var tail = url.split("/channel/")[1].split("/")[0].split("?")[0];
return /^UC[\w-]{22}$/.test(tail) ? tail : null;
}
/* Tek istekte bitmeyebilir: eski özel adresi olan kanallarda YouTube
* @handle'ı önce youtube.com/eskiad adresine yolluyor, kanal kimliği ancak
* bir sonraki adımda geliyor. Zincir kısa, sıçrama sayısı sınırlı: hem
* döngüye girmeyelim hem de tek kart için istek yağdırmayalım. */
var MAX_RESOLVE_HOPS = 3;
function resolveStep(url, raw, hops) {
return post("navigation/resolve_url", { url: url }).then(function (data) {
var endpoint = P.findFirst(data, "browseEndpoint") || {};
var id = endpoint.browseId || "";
if (id.indexOf("UC") === 0) return id;
/* Eski /user/ adresi olan kanallarda YouTube browseEndpoint yerine
* urlEndpoint ile başka bir adrese yolluyor. */
if (/^UC[\w-]{22}$/.test(id)) return id;
var next = (P.findFirst(data, "urlEndpoint") || {}).url || "";
if (next.indexOf("/channel/") >= 0) {
var t = next.split("/channel/")[1].split("/")[0].split("?")[0];
if (t.indexOf("UC") === 0) return t;
var direct = channelIdIn(next);
if (direct) return direct;
/* Sadece YouTube içinde kalan yönlendirmeler izlenir. */
var internal = /^https?:\/\/(www\.)?youtube\.com\//.test(next);
if (internal && next !== url && hops < MAX_RESOLVE_HOPS) {
return resolveStep(next, raw, hops + 1);
}
throw new Error("kanal çözülemedi: " + raw);
});
}
/* @handle veya kanal adresini UC... kimliğine çevirir. */
function resolveChannel(raw) {
raw = (raw || "").trim();
if (!raw) return Promise.reject(new Error("boş kanal adresi"));
if (/^UC[\w-]{22}$/.test(raw)) return Promise.resolve(raw);
var direct = channelIdIn(raw);
if (direct) return Promise.resolve(direct);
var url = raw;
if (url.indexOf("http") !== 0) {
url = "https://www.youtube.com/" + (url.charAt(0) === "@" ? url : "@" + url.replace(/^\/+/, ""));
}
return resolveStep(url, raw, 0);
}
/* Videonun kanal kimliği ve tam izlenme sayısı. Kartta kanal bağlantısı
* bulunamadığında son çare olarak kullanılır: video başına bir istek. */
* bulunamadığında son çare olarak kullanılır: video başına bir istek.
*
* Buna `player` ucu daha uygun görünür ve bir zamanlar öyleydi; oturum
* bilgisi göndermediğimiz için artık videoların büyük çoğunluğunda
* LOGIN_REQUIRED ("Sign in to confirm you're not a bot") dönüyor ve
* videoDetails hiç gelmiyor. `next` aynı iki alanı oturumsuz da veriyor.
* Yanıt büyük olduğu için bu uç bilerek son çare, ilk seçenek değil. */
function videoDetails(videoId) {
return post("player", { videoId: videoId }).then(function (data) {
var d = data.videoDetails || {};
return post("next", { videoId: videoId }).then(function (data) {
var owner = P.findFirst(data, "videoOwnerRenderer") || {};
var endpoint = P.findFirst(owner, "browseEndpoint") || {};
var channelId = endpoint.browseId || "";
var counter = P.findFirst(data, "videoViewCountRenderer") || {};
/* viewCount tam sayıyı verir ("63,087 views"); yoksa kısaltılmışa düş.
* Süren canlı yayında aynı alan "12,345 watching now" der; bu izlenme
* değil anlık izleyicidir, sayı sanılırsa skor uydurma çıkar. */
var countText = P.textOf(counter.viewCount) || P.textOf(counter.shortViewCount);
var views = /watching/i.test(countText) ? null : P.parseCountEn(countText);
var primary = P.findFirst(data, "videoPrimaryInfoRenderer") || {};
return {
channelId: d.channelId || null,
views: d.viewCount ? parseInt(d.viewCount, 10) : null,
title: d.title || "",
lengthSeconds: d.lengthSeconds ? parseInt(d.lengthSeconds, 10) : null,
isLive: !!d.isLiveContent
channelId: /^UC[\w-]{22}$/.test(channelId) ? channelId : null,
views: views,
title: P.textOf(primary.title),
publishedText: P.textOf(primary.dateText)
};
});
}
+17 -3
View File
@@ -98,6 +98,7 @@ eq(tiny.score, null, "3'ten az örnek: skor yok");
console.log("\n--- gerçek InnerTube çağrısı ---");
const CHANNEL = process.argv[2] || "UCXuqSBlHAE6Xw-yeJA0Tunw"; /* Linus Tech Tips */
let probe = null; /* videoDetails'ı sınamak için kanalın güncel bir videosu */
OBTube.channelTab(CHANNEL, "videos", 30).then((items) => {
console.log("videos sekmesi: " + items.length + " video");
const withViews = items.filter((v) => v.views);
@@ -108,6 +109,7 @@ OBTube.channelTab(CHANNEL, "videos", 30).then((items) => {
console.log(" " + v.videoId + " | " + OBParse.humanCount(v.views) + " | " +
(v.ageDays == null ? "?" : v.ageDays.toFixed(1) + " gün") + " | " + v.title.slice(0, 50));
}
probe = withViews.length ? withViews[0] : null;
const views = withViews.map((v) => v.views);
console.log("medyan: " + OBParse.humanCount(OBParse.median(views)));
if (withViews.length < 10) { fails++; console.log("FAIL izlenmesi çözülen video sayısı çok düşük"); }
@@ -118,10 +120,22 @@ OBTube.channelTab(CHANNEL, "videos", 30).then((items) => {
return OBTube.resolveChannel("@LinusTechTips");
}).then((id) => {
eq(id, CHANNEL, "handle -> kanal kimliği");
return OBTube.videoDetails("dQw4w9WgXcQ");
return OBTube.resolveChannel("@MesutCevik");
}).then((id) => {
/* Eski özel adresi olan bir kanal: YouTube @handle'ı önce
* youtube.com/mesutcevik adresine yolluyor, kimlik ancak ikinci adımda
* geliyor. Tek adımda çözen kod bu kanallarda "kanal çözülemedi" veriyor ve
* o kanalın hiçbir kartı puanlanmıyordu. */
eq(id, "UCOFafpmI_dt8SxisbKniN4A", "yönlendirmeli handle -> kanal kimliği");
/* Bilerek kanalın güncel bir videosu: sabit bir klasik ("dQw4w9WgXcQ")
* YouTube tarafında ayrıcalıklı davranıp uç bozulduğunda bile yanıt
* verebiliyor ve testi yanlış yere yeşil gösteriyordu. */
if (!probe) { fails++; console.log("FAIL sınanacak video bulunamadı"); return {}; }
return OBTube.videoDetails(probe.videoId);
}).then((d) => {
console.log("player ucu: kanal " + d.channelId + ", izlenme " + OBParse.humanCount(d.views));
if (!d.channelId) { fails++; console.log("FAIL player ucu kanal kimliği vermedi"); }
console.log("video ucu: kanal " + d.channelId + ", izlenme " + OBParse.humanCount(d.views));
eq(d.channelId, CHANNEL, "video ucu kanal kimliği");
if (!d.views) { fails++; console.log("FAIL video ucu izlenme vermedi"); }
console.log(fails ? "\n" + fails + " test başarısız" : "\nhepsi geçti");
process.exit(fails ? 1 : 0);
}).catch((e) => {