Greasy Fork is available in English.

HiAnime Watchlist Exporter (exports a json file) metadata slow

Exports your entire HiAnime.to watchlist. All categories with rich metadata pulled from each anime’s page.

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

You will need to install an extension such as Tampermonkey to install this script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==UserScript==
// @name         HiAnime Watchlist Exporter (exports a json file) metadata slow
// @author       ScriptKiddyMonkey
// @license      MIT
// @version      1
// @description  Exports your entire HiAnime.to watchlist. All categories with rich metadata pulled from each anime’s page.
// @match        https://hianime.to/user/watch-list*
// @grant        none
// @namespace    ScriptKiddyMonkey
// ==/UserScript==

(function () {
  'use strict';

  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

  const waitForSelector = async (selector) => {
    while (!document.querySelector(selector)) await sleep(200);
    return document.querySelector(selector);
  };

  const getCategoryFromElement = (el) => {
    const added = el.querySelector('.wl-item.added');
    if (!added) return 'Unknown';
    const map = {
      '1': 'Watching',
      '2': 'On-Hold',
      '3': 'Plan to Watch',
      '4': 'Dropped',
      '5': 'Completed',
    };
    return map[added.getAttribute('data-type')] || 'Unknown';
  };

  const getMetadataValue = (doc, label) => {
    const rows = doc.querySelectorAll('.anisc-info .item');
    for (let row of rows) {
      const title = row.querySelector('.item-head');
      if (title && title.textContent.trim().startsWith(label)) {
        const links = row.querySelectorAll('a');
        if (links.length > 0) {
          return Array.from(links).map((a) => a.textContent.trim());
        } else {
          const name = row.querySelector('.name');
          if (name) return name.textContent.trim();
        }
      }
    }
    return null;
  };

  const scrapeAnimeDetails = async (originalUrl) => {
    try {
      const url = originalUrl.replace('/watch/', '/');
      const res = await fetch(url);
      const html = await res.text();
      const doc = new DOMParser().parseFromString(html, 'text/html');

      const get = (selector) => {
        const el = doc.querySelector(selector);
        return el ? el.textContent.trim() : null;
      };

      const tick = {
        ageRating: null,
        subbedEpisodes: null,
        dubbedEpisodes: null,
        totalEpisodes: null,
        mediaType: null,
        duration: null,
      };

      const tickContainers = doc.querySelectorAll('.tick');

      tickContainers.forEach((container) => {
        container.querySelectorAll('.tick-item')?.forEach((item) => {
          const text = item.textContent.trim();
          if (item.classList.contains('tick-pg')) {
            tick.ageRating = text;
          } else if (item.classList.contains('tick-sub')) {
            tick.subbedEpisodes = parseInt(text) || null;
          } else if (item.classList.contains('tick-dub')) {
            tick.dubbedEpisodes = parseInt(text) || null;
          } else if (item.classList.contains('tick-eps')) {
            tick.totalEpisodes = parseInt(text) || null;
          }
        });

        container.querySelectorAll('span.item')?.forEach((span) => {
          const text = span.textContent.trim();
          if (!tick.mediaType && /^[A-Za-z\s]+$/.test(text)) {
            tick.mediaType = text;
          } else if (!tick.duration && /\d+m/.test(text)) {
            tick.duration = text;
          } else if (!tick.duration && tick.mediaType) {
            tick.duration = text;
          }
        });
      });

      return {
        description: get('.film-description .text') || get('.item .text'),
        genres: getMetadataValue(doc, 'Genres'),
        synonyms: getMetadataValue(doc, 'Synonyms') || [],
        japaneseTitle: getMetadataValue(doc, 'Japanese'),
        aired: getMetadataValue(doc, 'Aired'),
        premiered: getMetadataValue(doc, 'Premiered'),
        status: getMetadataValue(doc, 'Status'),
        score: getMetadataValue(doc, 'MAL Score'),
        studios: getMetadataValue(doc, 'Studios'),
        producers: getMetadataValue(doc, 'Producers'),
        duration: tick.duration,
        mediaType: tick.mediaType,
        totalEpisodes: tick.totalEpisodes,
        subbedEpisodes: tick.subbedEpisodes,
        dubbedEpisodes: tick.dubbedEpisodes,
        ageRating: tick.ageRating,
      };
    } catch (err) {
      console.error(`Failed to scrape details for ${originalUrl}`, err);
      return {};
    }
  };

  const scrapeEntireWatchlist = async () => {
    let page = 1;
    let animes = [];

    while (true) {
      const url = `${location.origin}${location.pathname}?page=${page}`;
      console.log(`Scraping page ${page}: ${url}`);
      const res = await fetch(url);
      const html = await res.text();
      const doc = new DOMParser().parseFromString(html, 'text/html');

      const cards = doc.querySelectorAll('.film_list-wrap .flw-item');
      if (!cards.length) break;

      for (const card of cards) {
        const a = card.querySelector('a.film-poster-ahref');
        const href = a?.href;
        const title = a?.getAttribute('oldtitle') || a?.title;
        const id = href?.split('/').pop();
        const img = card.querySelector('img.film-poster-img');
        const poster = img?.getAttribute('data-src') || img?.src || null;
        const category = getCategoryFromElement(card);

        animes.push({ id, title, url: href, category, poster });
      }

      page++;
      await sleep(1000);
    }

    return animes;
  };

  const scrapeDetailsForAll = async (list, btn) => {
    const updatedList = [];
    for (let i = 0; i < list.length; i++) {
      const entry = list[i];
      btn.textContent = `Scraping details... (${i + 1}/${list.length})`;
      const meta = await scrapeAnimeDetails(entry.url);
      updatedList.push({ ...entry, ...meta });
      await sleep(500);
    }
    return updatedList;
  };

  const exportWatchlist = async (btn) => {
    btn.disabled = true;
    btn.textContent = 'Gathering watchlist...';

    try {
      const baseList = await scrapeEntireWatchlist();
      const fullList = await scrapeDetailsForAll(baseList, btn);

      const blob = new Blob([JSON.stringify(fullList, null, 2)], {
        type: 'application/json',
      });

      const username = location.pathname.split('/')[2] || 'user';
      const date = new Date().toISOString().split('T')[0];
      const filename = `HiAnime_Watchlist_${date}.json`;

      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = filename;
      document.body.appendChild(a);
      a.click();
      a.remove();
      URL.revokeObjectURL(url);

      btn.textContent = 'Export Complete!';
    } catch (e) {
      console.error('Export failed:', e);
      btn.textContent = 'Export Failed. Check console.';
    }
  };

  const addExportButton = () => {
    const btn = document.createElement('button');
    btn.textContent = 'Export Full Watchlist';
    btn.style =
      'position:fixed;bottom:20px;right:20px;padding:10px 20px;background:#201f2d;color:#fff;border:none;border-radius:8px;z-index:99999;font-weight:bold;box-shadow:0 4px 12px rgba(0,0,0,0.3);cursor:pointer';

    btn.onclick = () => exportWatchlist(btn);
    document.body.appendChild(btn);
  };

  waitForSelector('.film_list-wrap').then(addExportButton);
})();