From 1e1c0920552784bd2ca6c33948c03ab96fc833cb Mon Sep 17 00:00:00 2001 From: yusufipk Date: Sat, 22 Aug 2026 15:31:24 +0300 Subject: [PATCH] Follow the redirect when a handle resolves to a legacy vanity address MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/content.js | 31 ++++++++++++++++++++---------- src/innertube.js | 49 +++++++++++++++++++++++++++++++----------------- test/check.js | 7 +++++++ 3 files changed, 60 insertions(+), 27 deletions(-) diff --git a/src/content.js b/src/content.js index 78732d5..78a1b1c 100644 --- a/src/content.js +++ b/src/content.js @@ -107,20 +107,31 @@ } /* 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ç + * 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(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 OBStore.readChannelId(found.handle).then(function (cached) { - if (cached) return cached; - return OBTube.resolveChannel(found.handle).then(function (id) { - OBStore.writeChannelId(found.handle, id); - return id; - }); - }); - } + if (found && found.handle) return resolveHandle(found.handle); var key = "v:" + videoId; return OBStore.readChannelId(key).then(function (cached) { if (cached) return cached; diff --git a/src/innertube.js b/src/innertube.js index 288d965..d2bde23 100644 --- a/src/innertube.js +++ b/src/innertube.js @@ -268,32 +268,47 @@ var OBTube = (function () { return post("browse", { browseId: channelId, params: params }).then(round); } + 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 (/^UC[\w-]{22}$/.test(id)) return id; + var next = (P.findFirst(data, "urlEndpoint") || {}).url || ""; + 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); - 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 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 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. */ - 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; - } - throw new Error("kanal çözülemedi: " + raw); - }); + return resolveStep(url, raw, 0); } /* Videonun kanal kimliği ve tam izlenme sayısı. Kartta kanal bağlantısı diff --git a/test/check.js b/test/check.js index 1baccf2..830d625 100644 --- a/test/check.js +++ b/test/check.js @@ -120,6 +120,13 @@ OBTube.channelTab(CHANNEL, "videos", 30).then((items) => { return OBTube.resolveChannel("@LinusTechTips"); }).then((id) => { eq(id, CHANNEL, "handle -> kanal kimliği"); + 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. */