Greasy Fork

Ao3 Auto Bookmarker

Allows for autofilled bookmark summary and tags on Ao3.

目前为 2025-04-01 提交的版本。查看 最新版本

// ==UserScript==
// @name        Ao3 Auto Bookmarker
// @description Allows for autofilled bookmark summary and tags on Ao3.
// @namespace   Ao3
// @match       http*://archiveofourown.org/works/*
// @match       http*://archiveofourown.org/series/*
// @grant       none
// @version     1.2
// @author      Legovil
// @license     MIT
// ==/UserScript==

// Settings for the script, allowing customization of which features are enabled.
const settings = {
  generateTitleNote: true,
  generateSummaryNote: true,
  checkRecBox: false,
  checkPrivateBox: false,
  getRating: true,
  getArchiveWarnings: false,
  getCategoryTags: true,
  getFandomTags: false,
  getRelationshipTags: false,
  getCharacterTags: false,
  getAdditionalTags: false,
  generateWordCountTag: true,
  appendToExistingNote: false,
  appendToExistingTags: false,
  usingAo3Extensions: true,
};

// Word count boundaries for generating word count tags.
const wordCountBounds = [1000, 5000, 10000, 50000, 100000, 500000];

// Enum for bookmark types.
const BookmarkType = Object.freeze({
  WORK: 'Work',
  SERIES: 'Series',
});

(function() {
  'use strict';

  // Determine the type of bookmark based on the URL.
  const bookmarkType = checkBookmarkType(window.location.href);

  // If the bookmark type is not found, cancel the generation process.
  if (bookmarkType === null) {
    console.error('Bookmark type not found. Cancelling bookmark generation.');
    return;
  }

  // Generate notes and tags, and handle checkboxes based on the bookmark type.
  generateNotes(bookmarkType);
  generateTags();
  handleCheckBoxes();
})();

/**
 * Checks the type of bookmark based on the URL.
 * @param {string} url The URL to check.
 * @return {string|null} The bookmark type or null if not found.
 */
function checkBookmarkType(url) {
  if (url.includes('/works/')) {
    console.log('Found Work Bookmark.');
    return BookmarkType.WORK;
  }
  if (url.includes('/series/')) {
    console.log('Found Series Bookmark.');
    return BookmarkType.SERIES;
  }
  return null;
}

/**
 * Generates notes for the bookmark.
 * @param {string} bookmarkType The type of bookmark.
 */
function generateNotes(bookmarkType) {
  const notesElement = document.getElementById('bookmark_notes');
  if (!notesElement) {
    console.error('Notes element not found. Cancelling notes generation.');
    return;
  }

  // Generate title and summary notes based on settings.
  const notes = [
    settings.generateTitleNote && generateTitleNote(bookmarkType),
    settings.generateSummaryNote && generateSummaryNote(bookmarkType),
  ].filter(Boolean).join('\n\n');

  // Append or replace existing notes based on settings.
  notesElement.value = settings.appendToExistingNote ? `${notesElement.value}\n\n${notes}` : notes;
}

/**
 * Generates the title note for the bookmark.
 * @param {string} bookmarkType The type of bookmark.
 * @return {string} The generated title note.
 */
function generateTitleNote(bookmarkType) {
  const titleQuery = bookmarkType === BookmarkType.WORK
    ? '.title.heading'
    : 'ul.series a';

  const title = document.querySelector(titleQuery);
  if (!title) {
    console.warn('Title not found. Cancelling Title Note generation.');
    return '';
  }

  const bylineQuery = bookmarkType === BookmarkType.WORK
    ? '.byline.heading a'
    : '.series.meta.group a';

  const byline = document.querySelector(bylineQuery);
  if (!byline) {
    console.warn('Byline not found. Cancelling Title Note generation.');
    return '';
  }

  return `${title.innerHTML.link(window.location.href)} by ${byline.outerHTML}.`;
}

/**
 * Generates the summary note for the bookmark.
 * @param {string} bookmarkType The type of bookmark.
 * @return {string} The generated summary note.
 */
function generateSummaryNote(bookmarkType) {
  const summaryQuery = bookmarkType === BookmarkType.WORK
    ? '.summary.module .userstuff'
    : '.series.meta.group .userstuff';

  const summary = document.querySelector(summaryQuery);
  if (!summary) {
    console.warn('No summary found. Cancelling summary note generation.');
    return '';
  }

  return `Summary: ${summary.innerText}`;
}

/**
 * Generates tags for the bookmark.
 */
function generateTags() {
  const tagsElement = document.getElementById('bookmark_tag_string_autocomplete');
  if (!tagsElement) {
    console.error('Tags input element not found. Cancelling bookmark tag generation.');
    return;
  }

  // Remove existing tags if the setting is not to append to them.
  if (!settings.appendToExistingTags) {
    document.querySelectorAll('.added.tag a').forEach(tagLink => tagLink.click());
  }

  console.log('Tags input element found.');
  tagsElement.value = [
    settings.getArchiveWarnings && getTagsFromString('warning tags'),
    settings.getCategoryTags && getTagsFromString('category tags'),
    settings.getFandomTags && getTagsFromString('fandom tags'),
    settings.getRelationshipTags && getTagsFromString('relationship tags'),
    settings.getCharacterTags && getTagsFromString('character tags'),
    settings.getAdditionalTags && getTagsFromString('freeform tags'),
    settings.generateWordCountTag && generateWordCountTag(),
  ].filter(Boolean).join(', ');
}

/**
 * Gets tags from a specific class name.
 * @param {string} tagClassName The class name to get tags from.
 * @return {string} The generated tags.
 */
function getTagsFromString(tagClassName) {
  const tagList = document.getElementsByClassName(tagClassName)[1]?.getElementsByClassName('tag');
  if (!tagList || tagList.length === 0) {
    console.error(`Tags element not found. Cancelling ${tagClassName} generation.`);
    return '';
  }
  return Array.from(tagList).map(tag => tag.text).join(', ');
}

/**
 * Generates a word count tag based on the word count boundaries.
 * @return {string} The generated word count tag.
 */
function generateWordCountTag() {
  const wordCountElement = settings.usingAo3Extensions ? document.getElementsByClassName('words')[2] : document.getElementsByClassName('words')[1];
  if (!wordCountElement || wordCountElement.innerText === 'Words:') {
    console.error('Word count not found. Cancelling word count tag generation. Check to see if Ao3 Extensions setting is toggled correctly.');
    return '';
  }
  const wordCount = wordCountElement.innerText.replaceAll(',', '').replaceAll(' ', '');
  console.log(`Word count: ${wordCount}`);
  let lowerBound = wordCountBounds[0];
  if (wordCount < lowerBound) {
    return `< ${lowerBound}`;
  }

  for (const upperBound of wordCountBounds) {
    if (wordCount < upperBound) {
      return `${lowerBound} - ${upperBound}`;
    }
    lowerBound = upperBound;
  }
  return `> ${wordCountBounds[wordCountBounds.length - 1]}`;
}

/**
 * Handles the state of checkboxes based on settings.
 */
function handleCheckBoxes() {
  const recBox = document.getElementById('bookmark_rec');
  if (settings.checkRecBox && recBox) {
    console.log('Checking rec box.');
    recBox.checked = true;
  }

  const privateBox = document.getElementById('bookmark_private');
  if (settings.checkPrivateBox && privateBox) {
    console.log('Checking private box.');
    privateBox.checked = true;
  }
}