Jellyfin Forum
Collections by Folder - Printable Version

+- Jellyfin Forum (https://forum.jellyfin.org)
+-- Forum: Development (https://forum.jellyfin.org/f-development)
+--- Forum: Plugin Development (https://forum.jellyfin.org/f-plugin-development)
+--- Thread: Collections by Folder (/t-collections-by-folder)



Collections by Folder - Solas - 2025-10-05

The plugin scans the specified folders and automatically creates collections based on the folder name.


https://raw.githubusercontent.com/Solas79/folder-collection/main/manifest.json


RE: Collections by Folder - Syunic - 2025-10-25

Does this work with the newest version of jellyfin? cant seem to get it to work.


RE: Collections by Folder - Solas - 2025-10-25

I add an prerelease (v0.3.6) today.
It should work. However, you have to first click on the puzzle piece and then on settings.
That's why it's still a pre-release. I haven't found a suitable solution yet.Don't forget to check the box Group movies into collections in the dashboard, libraries, display

Ich habe heute eine Prerelaese (v0.3.6) hochgeladen.
Es sollte funktionieren. Allerdings muss man zuerst auf das Puzzleteil und dann auf Einstellungen klicken.
Deshalb ist es noch eine Vorabversion. Ich habe noch keine passende Lösung gefunden. Vergiss nicht, im Dashboard, Bibliotheken, Anzeige das Kontrollkästchen „Filme in Sammlungen gruppieren“ zu aktivieren. So hast du die Filme einmal gruppiert als Ordner und einmal als Sammlung. Dann fliegen die nicht als einzelne Filme in der Bibliothek rum


RE: Collections by Folder - Solas - 2025-11-21

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);
})();