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. */