// ==UserScript== // @name Trakt.tv | Enhanced Title Metadata // @description Adds links of filtered search results to the metadata section (languages, genres, networks, studios, writers, certification, year) on title summary pages, similar to the vip feature. Also adds a country flag and allows for "combined" searches by clicking on the labels. See README for details. // @version 1.0.5 // @namespace https://github.com/Fenn3c401 // @author Fenn3c401 // @license GPL-3.0-or-later // @homepageURL https://github.com/Fenn3c401/Trakt.tv-Userscript-Collection#readme // @supportURL https://github.com/Fenn3c401/Trakt.tv-Userscript-Collection/issues // @updateURL https://update.greasyfork.org/scripts/550076.meta.js // @downloadURL https://raw.githubusercontent.com/Fenn3c401/Trakt.tv-Userscript-Collection/main/userscripts/dist/fyk2l3vj.min.user.js // @icon https://trakt.tv/assets/logos/logomark.square.gradient-b644b16c38ff775861b4b1f58c1230f6a097a2466ab33ae00445a505c33fcb91.svg // @match https://trakt.tv/* // @match https://classic.trakt.tv/* // @run-at document-start // @grant unsafeWindow // @grant GM_info // @grant GM_addStyle // @grant GM_openInTab // @grant GM_getValue // @grant GM_setValue // ==/UserScript== /* README > Based on sergeyhist's [Trakt.tv Clickable Info](https://github.com/sergeyhist/trakt-scripts/blob/main/trakt-info.user.js) userscript. ### General - By installing the [Trakt.tv | Trakt API Wrapper](f785bub0.md) userscript you can speed up the studios data fetching. - By clicking on the label for languages, genres, networks, studios and writers, you can make a search for all their respective values combined, ANDed for genres, languages and writers, ORed for networks and studios. For example if the genres are "Crime" and "Drama", then a label search will return a selection of other titles that also have the genres "Crime" AND "Drama". - The writers label search was mostly added as an example of how to search for filmography intersections with trakt's search engine (there's no official tutorial about this, just some vague one liner in the api docs about how `+ - && || ! ( ) { } [ ] ^ " ~ * ? : /` have "special meaning" when used in a query). It's much more interesting with actors e.g. [Movies with Will Smith and Alan Tudyk](https://trakt.tv/search/movies?query=%22Will%20Smith%22+%22Alan%20Tudyk%22&fields=people). - The title's certification links to the respective `/parentalguide` imdb page (which contains descriptions of nude scenes, graphic content etc.). - The title's year links to the search page for other titles from the same year. - The search results default to either the "movies" or "shows" search category depending on the type of the current title. - A "+ n more" button is added for networks when needed (some anime have more than a dozen listed). - Installing the [Trakt.tv | Partial VIP Unlock](x70tru7b.md) userscript will allow free users to further modify the applied advanced filters on the linked search pages. - This script won't work for vip users. */ "use strict";let $,traktApiWrapper;const logger={_defaults:{title:(typeof moduleName<"u"?moduleName:GM_info.script.name).replace("Trakt.tv","Userscript"),toast:!0,toastrOpt:{positionClass:"toast-top-right",timeOut:1e4,progressBar:!0},toastrStyles:"#toast-container#toast-container a { color: #fff !important; border-bottom: dotted 1px #fff; }"},_print(n,t,l="",A={}){const{title:b=this._defaults.title,toast:C=this._defaults.toast,toastrOpt:m,toastrStyles:y="",consoleStyles:w="",data:x}=A,k=`${l}${x!==void 0?" See console for details.":""}`;console[n](`%c${b}: ${l}`,w,...x!==void 0?[x]:[]),C&&unsafeWindow.toastr?.[t](k,b,{...this._defaults.toastrOpt,...m})},info(n,t){this._print("info","info",n,t)},success(n,t){this._print("info","success",n,{consoleStyles:"color:#00c853;",...t})},warning(n,t){this._print("warn","warning",n,t)},error(n,t){this._print("error","error",n,t)}},gmStorage={...GM_getValue("enhancedTitleMetadata")};GM_setValue("enhancedTitleMetadata",gmStorage),addStyles(),document.addEventListener("turbo:load",async()=>{if(!/^\/(shows|movies)\//.test(location.pathname)||($??=unsafeWindow.jQuery,traktApiWrapper??=unsafeWindow.userscriptTraktApiWrapper,!$))return;const n=$("#overview .additional-stats > li"),t=location.pathname.split("/").filter(Boolean);if(!n.length)return;const l=$("#summary-wrapper .year");l.parent().is("a")&&l.insertAfter(l.parent()),l.wrapAll(``),$("#summary-wrapper div.certification").wrap(``);const b=n.filter((a,e)=>$(e).find("label").text().toLowerCase()==="writers");b.find("label").wrap(`a.textContent).join("%22+%22")}%22&fields=people">`);const C=n.filter(':has([itemprop="genre"])'),m=[];C.find('[itemprop="genre"]').each((a,e)=>{m[a]=$(e).text().toLowerCase().replaceAll(" ","-"),$(e).wrap(``)}),m.length>1&&C.find("label").wrap(``);const y=n.filter((a,e)=>$(e).find("label").text().toLowerCase()==="country");let w;if(y.length){const a=await getAllCountriesMap(),e=y.contents().get(-1)?.textContent;if(w=a[e],w){const r=unsafeWindow.watchnowAllCountries?.[w]?.image;r&&y.children().last().after(``),y.contents().filter((o,s)=>!$(s).is("meta, label")).wrapAll(``)}else gmStorage.allCountriesMap=null,GM_setValue("enhancedTitleMetadata",gmStorage),logger.error("Failed to match title country. Cached countries have been cleared. Reload page to try again.")}const x=n.filter((a,e)=>$(e).find("label").text().toLowerCase().startsWith("language")),k={};if(x.length){const a=await getAllLanguagesArrSorted(),e=Object.fromEntries(a);let r=x.contents().get(-1).textContent;if(a.forEach(([o,s],i)=>{const g=new RegExp(`${RegExp.escape(s)}(, |$)`);g.test(r)&&(k[r.indexOf(s)]=o,r=r.replace(g,O=>" ".repeat(O.length)))}),r.trim())gmStorage.allLanguagesArrSorted=null,GM_setValue("enhancedTitleMetadata",gmStorage),logger.error(`Failed to match all title languages (ORIGINAL: ${x.contents().get(-1).textContent} REMAINDER: ${r.trim()}). Cached languages have been cleared. Reload page to try again.`);else{const o=Object.values(k);x.contents().last().replaceWith(o.map(s=>`${e[s]}`).join(", ")),o.length>1&&x.find("label").wrap(``)}}const M=n.filter((a,e)=>$(e).find("label").text().toLowerCase().startsWith("network")),L=n.filter((a,e)=>/airs|aired|premiered/i.test($(e).find("label").text())).first();if(M.length&&t[3]!=="all"){const a={},e=await getAllNetworksArrSorted(),r=Object.fromEntries(e);let o=M.contents().get(-1).textContent;if(e.forEach(([s,{name:i,countryId:g}],O)=>{const c=new RegExp(`${RegExp.escape(i)}(, |$)`);c.test(o)&&(g===w||Object.hasOwn(k,g)||i!==e[O+1]?.[1].name)&&(a[o.indexOf(i)]=s,o=o.replace(c,d=>" ".repeat(d.length)))}),o.trim())gmStorage.allNetworksArrSorted=null,GM_setValue("enhancedTitleMetadata",gmStorage),logger.error(`Failed to match all title networks (ORIGINAL: ${M.contents().get(-1).textContent} REMAINDER: ${o.trim()}). Cached networks have been cleared. Reload page to try again.`);else{const s=Object.values(a);M.contents().last().replaceWith(s.map(i=>`${r[i].name}${r[i].countryId?` (${r[i].countryId.toUpperCase()})`:""}`).join("")),s.length>1&&(M.find("label").wrap(``),$(` + ${s.length-1} more`).insertAfter(M.children().eq(1)).nextAll().wrapAll('')),M.find("a:not(:has(label), [onclick])").slice(1).before(", ")}}else if(L.text().includes(" on ")&&t[3]!=="all"){const a=await getAllNetworksArrSorted(),e=L.contents().last().text().split(" on ")[1],r=e?a.find(([o,{name:s,countryId:i}],g)=>new RegExp(`${RegExp.escape(s)}(, |$)`).test(e)&&(i===w||Object.hasOwn(k,i)||s!==a[g+1]?.[1].name)):null;r?(L.contents().last().remove(),L.append(` on ${r[1].name}${r[1].countryId?` (${r[1].countryId.toUpperCase()})`:""}`)):(gmStorage.allNetworksArrSorted=null,GM_setValue("enhancedTitleMetadata",gmStorage),logger.error(`Failed to match title network (${e}). Cached networks have been cleared. Reload page to try again.`))}const p=n.filter((a,e)=>$(e).find("label").text().toLowerCase().startsWith("studio"));if(p.length)if(traktApiWrapper){let a=!1;const e=async function(r){if(a)return;a=!0,r?.preventDefault(),unsafeWindow.showLoading?.();const o=await traktApiWrapper[t[0]].studios({id:$(".summary-user-rating").attr(`data-${t[0].slice(0,-1)}-id`)}),s=o.map(i=>i.ids.trakt).join();if(unsafeWindow.hideLoading?.(),r){const i=`/search/${t[0]}?studio_ids=${$(this).find("label").length?s:o[0].ids.trakt}`;r.type==="click"?location.href=i:r.originalEvent.button===1&&GM_openInTab(location.origin+i,{setParent:!0})}p.children().eq(0).attr("href",`/search/${t[0]}?studio_ids=${s}`),p.children().eq(1).attr("href",`/search/${t[0]}?studio_ids=${o[0].ids.trakt}`),p.find(".studios-more").html(o.slice(1).map(i=>`, ${i.name}`))};p.find("label").wrap($('').one("click auxclick",e)),p.contents().eq(1).wrap($('').one("click auxclick",e)),p.find(".studios-expand").one("click",()=>e())}else{const a=new Set,e=p.find(".studios-more"),r=p.find(".studios-expand"),o=e.text().split(", ").slice(1),s=+r.text().match(/\d+/)?.[0]||null,i=c=>fetch("/autocomplete/studios?query="+encodeURIComponent(c)).then(d=>d.json()).then(d=>Object.fromEntries(d.map(({label:f,value:u,tag:h})=>[f,+u,h?.toLowerCase()??null]).sort(([f,u,h],[S,_,E])=>f===S?(h&&(h===w||Object.hasOwn(k,h)))-(E&&(E===w||Object.hasOwn(k,E)))||_-u:0))),g=async function(c){c?.preventDefault(),$(this).off(),unsafeWindow.showLoading?.();const d=$(this).text(),f=await i(d),u=f[d];if(unsafeWindow.hideLoading?.(),u){a.add(u);const h=`/search/${t[0]}?studio_ids=${u}`;c&&(c.type==="click"?location.href=h:c.originalEvent.button===1&&GM_openInTab(location.origin+h,{setParent:!0})),$(this).attr("href",h)}else logger.error("Failed to match title studio: "+d,{data:f})},O=async()=>{if(a.size>1)return;unsafeWindow.showLoading?.();const c=await Promise.all(o.map(f=>i(f).then(u=>[f,u])));let d=-1;unsafeWindow.hideLoading?.(),e.html(c.map(([f,u],h)=>{if(h<=d)return null;let S;for(let _=h;_${S[0]}`;throw logger.error("Failed to match all title studios. Could not match: "+c[h][0],{data:u}),new Error("Failed to match all title studios.")}).join(""))};p.contents().eq(1).wrap($('').on("click auxclick",g)),s&&(s===1?e.text(", ").append($(`${o.join(", ")}`).on("click auxclick",g)):s===o.length?(e.empty(),o.forEach(c=>e.append(", ",$(`${c}`).on("click auxclick",g)))):r.one("click",O),p.find("label").wrap('').parent().on("click auxclick",async function(c){c.preventDefault(),$(this).off(),await Promise.all([...p.find('a[href="#"]:not(:has(label), .studios-expand)').get().map(f=>g.call(f)),O()]);const d=`/search/${t[0]}?studio_ids=${Array.from(a).join(",")}`;c.type==="click"?location.href=d:c.originalEvent.button===1&&GM_openInTab(location.origin+d,{setParent:!0}),$(this).attr("href",d)}))}},{capture:!0});async function getAllCountriesMap(){if(!gmStorage.allCountriesMap){const n=await fetch("/search/movies").then(t=>t.text()).then(t=>new DOMParser().parseFromString(t,"text/html"));gmStorage.allCountriesMap=JSON.stringify(Object.fromEntries($(n).find("#filter-countries").children().get().map(t=>[$(t).text(),$(t).attr("value").toLowerCase()]))),GM_setValue("enhancedTitleMetadata",gmStorage)}return JSON.parse(gmStorage.allCountriesMap)}async function getAllLanguagesArrSorted(){if(!gmStorage.allLanguagesArrSorted){const n=await fetch("/search/movies").then(t=>t.text()).then(t=>new DOMParser().parseFromString(t,"text/html"));gmStorage.allLanguagesArrSorted=JSON.stringify($(n).find("#filter-languages").children().get().map(t=>[$(t).attr("value"),$(t).text()]).sort(([,t],[,l])=>l.length-t.length)),GM_setValue("enhancedTitleMetadata",gmStorage)}return JSON.parse(gmStorage.allLanguagesArrSorted)}async function getAllNetworksArrSorted(){if(!gmStorage.allNetworksArrSorted){const n=await fetch("/search/shows").then(l=>l.text()).then(l=>new DOMParser().parseFromString(l,"text/html")),t=new Intl.Collator;gmStorage.allNetworksArrSorted=JSON.stringify($(n).find("#filter-network_ids").children().get().map(l=>$(l).text()?[+$(l).attr("value"),{name:$(l).text(),countryId:$(l).attr("data-tag")?.toLowerCase()}]:null).filter(Boolean).sort(([l,{name:A,countryId:b}],[C,{name:m,countryId:y}])=>m.length-A.length||t.compare(A,m)||(y&&1)-(b&&1)||C-l)),GM_setValue("enhancedTitleMetadata",gmStorage)}return JSON.parse(gmStorage.allNetworksArrSorted)}function addStyles(){GM_addStyle(` #overview .additional-stats .country-flag { width: 20px !important; margin: -2px 5px 0 0 !important; transition: transform .5s ease; } #overview .additional-stats a:hover > .country-flag { transform: scale(1.1); } :is(#info-wrapper .additional-stats a > label, #summary-wrapper a > .year):hover { color: var(--link-color) !important; cursor: pointer !important; } #summary-wrapper a:has(> .certification):hover { color: #fff !important; } `)}