• Login
  • Register
  • Login Register
    Login
    Username/Email:
    Password:
    Or login with a social network below
  • Forum
  • Website
  • GitHub
  • Status
  • Translation
  • Features
  • Team
  • Rules
  • Help
  • Feeds
User Links
  • Login
  • Register
  • Login Register
    Login
    Username/Email:
    Password:
    Or login with a social network below

    Useful Links Forum Website GitHub Status Translation Features Team Rules Help Feeds
    Jellyfin Forum Development Plugin Development Collections by Folder

     
    • 0 Vote(s) - 0 Average

    Collections by Folder

    Generate Collections by Foldername
    Solas
    Offline

    Junior Member

    Posts: 4
    Threads: 1
    Joined: 2025 Oct
    Reputation: 0
    Country:Germany
    #4
    2 hours ago
    Small gimmick for my collections:
     
    I'm also using JS-Injector. In collections, a circle is still displayed in the upper right corner showing the number of unwatched movies. The script displays a red circle in the upper left corner showing the number of movies in the collection. 
    Script was created with KI, fell free to change it.

             

    Just insert in Javascript Injection Plugin:

    Code:
    // == Jellyfin Film-Collection Badge Script (Series ausgeschlossen, Schauspieler ignoriert) ==
    (async function(){
      console.clear();
      console.log('Film-Collection Badge (Series ausgeschlossen) startet...');
      const SERVER_URL = 'http://XXX.XXX.XXX.XXX:8096'; // Jellyfin-Server-Adresse
      const CARD_SELECTOR = '.cardScalable';
      const BADGE_CLASS = 'collection-count-badge-js';
      const CACHE_TTL_MS = 5 * 60 * 1000;
      // --- Badge CSS ---
      if (!document.getElementById('collection-count-badge-js-style')) {
        const s = document.createElement('style');
        s.id = 'collection-count-badge-js-style';
        s.textContent = `
          .${BADGE_CLASS}{
            position:absolute;
            top:6px;
            left:6px;
            min-width:30px;
            height:30px;
            border-radius:50%;
            background:#ff3b30;
            color:#fff;
            display:flex;
            align-items:center;
            justify-content:center;
            font-weight:700;
            font-size:13px;
            z-index:99999;
          }
        `;
        document.head.appendChild(s);
      }
      const cache = new Map();
      // --- ID aus Karte extrahieren ---
      function extractIdFromCard(card){
        if (!card) return null;
        try {
          if (card.dataset && (card.dataset.itemid || card.dataset.id || card.dataset.folderid))
            return card.dataset.itemid || card.dataset.id || card.dataset.folderid;
          const a = card.querySelector('a[href*="details?id="]') || card.querySelector('a[href*="details"]');
          if (a){
            const href = a.getAttribute('href')||'';
            const q = href.split('?')[1] || href.split('#')[1] || '';
            return new URLSearchParams(q).get('id') || null;
          }
        } catch(e){}
        return null;
      }
      // --- Prüfen, ob Item Collection ist und wie viele Movies sie enthält ---
      async function fetchMovieCountForCollection(id, apiKey){
        if (!id) return 0;
        const cached = cache.get(id);
        if (cached && (Date.now() - cached.ts) < CACHE_TTL_MS) return cached.count;
        try {
          const detailsUrl = `${SERVER_URL}/Items/${encodeURIComponent(id)}`;
          const detRes = await fetch(detailsUrl, { headers: {'X-Emby-Token': apiKey} });
          if (!detRes.ok) { cache.set(id, {count:0, ts:Date.now()}); return 0; }
          const details = await detRes.json();
          // KEIN Badge für Movies, Series oder Schauspieler
          if (details && (details.Type === 'Movie' || details.Type === 'Series' || details.Type === 'Person')) {
            cache.set(id, {count:0, ts:Date.now()});
            return 0;
          }
          // Falls es ein Folder/Collection ist: nur Movie-Children zählen
          const listUrl = `${SERVER_URL}/Items?ParentId=${encodeURIComponent(id)}&Recursive=false&IncludeItemTypes=Movie`;
          const listRes = await fetch(listUrl, { headers: {'X-Emby-Token': apiKey} });
          if (!listRes.ok) { cache.set(id, {count:0, ts:Date.now()}); return 0; }
          const listData = await listRes.json();
          let movieCount = 0;
          if (listData && typeof listData.TotalRecordCount === 'number') movieCount = listData.TotalRecordCount;
          else if (listData && Array.isArray(listData.Items)) movieCount = listData.Items.length;
          movieCount = Number.isFinite(movieCount) ? Math.max(0, Math.floor(movieCount)) : 0;
          cache.set(id, {count: movieCount, ts: Date.now()});
          return movieCount;
        } catch (e) {
          console.error('fetchMovieCountForCollection error', e);
          cache.set(id, {count:0, ts:Date.now()});
          return 0;
        }
      }
      // --- Badge an Karte anhängen ---
      function attachBadge(card, count){
        if (!card || count <= 0) return;
        if (card.querySelector(`.${BADGE_CLASS}`)) return;
        if (getComputedStyle(card).position === 'static') card.style.position = 'relative';
        const b = document.createElement('div');
        b.className = BADGE_CLASS;
        b.textContent = String(count);
        card.appendChild(b);
      }
      // --- Karte annotieren ---
      async function annotateCard(card, apiKey){
        if (!card || card.dataset.__ct_annotated === '1') return;
        card.dataset.__ct_annotated = '1';
        const id = extractIdFromCard(card);
        if (!id) return;
        // Nur echte Movie-Collections bekommen ein Badge
        const movieCount = await fetchMovieCountForCollection(id, apiKey);
        if (movieCount > 0) attachBadge(card, movieCount);
      }
      // --- Initial annotieren ---
      async function annotateAll(apiKey){
        const cards = Array.from(document.querySelectorAll(CARD_SELECTOR));
        console.log('Film-Collection Annotator — Cards gefunden:', cards.length);
        cards.forEach((c, i) => setTimeout(() => annotateCard(c, apiKey).catch(err => console.error(err)), i * 60));
      }
      // --- MutationObserver für dynamisch nachgeladene Karten ---
      function observeMutations(apiKey){
        new MutationObserver(muts => {
          for (const m of muts) {
            for (const n of m.addedNodes) {
              if (n.nodeType !== 1) continue;
              if (n.matches && n.matches(CARD_SELECTOR)) annotateCard(n, apiKey);
              else if (n.querySelectorAll) {
                const found = n.querySelectorAll(CARD_SELECTOR);
                if (found && found.length) found.forEach(c => annotateCard(c, apiKey));
              }
            }
          }
        }).observe(document.body, { childList: true, subtree: true });
      }
      // --- Script starten ---
      const apiKey = await (async function waitForKey(timeout = 15000, interval = 200) {
        const start = Date.now();
        let key = null;
        while (!key && (Date.now() - start) < timeout) {
          try { key = (window.ApiClient?.accessToken?.() || window.JellyfinClient?.accessToken?.()); } catch(e){}
          if (!key) await new Promise(r => setTimeout(r, interval));
        }
        return key;
      })();
      if (!apiKey) {
        console.error('Kein Browser-Key gefunden – Badge kann nicht angezeigt werden.');
        alert('Kein Browser-Key gefunden!');
        return;
      }
      console.log('Browser-Key gefunden:', apiKey);
      annotateAll(apiKey);
      observeMutations(apiKey);
    })();
    « Next Oldest | Next Newest »

    Users browsing this thread: 1 Guest(s)


    Messages In This Thread
    Collections by Folder - by Solas - 2025-10-05, 11:36 PM
    RE: Collections by Folder - by Syunic - 2025-10-25, 10:14 AM
    RE: Collections by Folder - by Solas - 2025-10-25, 01:30 PM
    RE: Collections by Folder - by Solas - 2 hours ago

    • View a Printable Version
    • Subscribe to this thread
    Forum Jump:

    Home · Team · Help · Contact
    © Designed by D&D - Powered by MyBB
    L


    Jellyfin

    The Free Software Media System

    Linear Mode
    Threaded Mode