Hey all. I’ve created for me a bookmarklet to automate the attribution from e621 posts and I figured I’m gonna share it.

The bookmarklet creates the Markdown syntax for including an image from e621 with the correct artist name and all his pages. It won’t generate an output for all artists that are also tagged with “conditional dnp”, there you have to manually look if you can post the image or not.

Disclaimer: I’m a random dude from the internet, providing you some code to run on your device. Please be aware that this is an dangerous action, if you don’t know what you are doing. Just as an example, discord tokens where stolen with such bookmarklets. But I append also the source code, so you can have a look for yourself. Also, I don’t take any responsibility for potential damage my code could do (even though I cannot imagine how this code should harm anything).

Bookmarklet

javascript:(function()%7Basync%20function%20run()%20%7B%0D%0A%20%20let%20location%20%3D%20window.location%3B%0D%0A%20%20try%20%7B%0D%0A%20%20%20%20url%20%3D%20new%20URL(location)%3B%0D%0A%20%20%20%20if%20(url.hostname%20%3D%3D%3D%20%22e621.net%22%20%7C%7C%20url.hostname%20%3D%3D%3D%20%22www.e621.net%22)%20%7B%0D%0A%20%20%20%20%20%20prepareOutput()%3B%0D%0A%20%20%20%20%20%20const%20text%20%3D%20await%20parsePost(url)%3B%0D%0A%20%20%20%20%20%20insertMD(text)%3B%0D%0A%20%20%20%20%7D%20else%20%7B%0D%0A%20%20%20%20%20%20alert(%22This%20script%20only%20works%20with%20e621.%22)%3B%0D%0A%20%20%20%20%7D%0D%0A%20%20%7D%20catch%20(error)%20%7B%0D%0A%20%20%20%20alert(%22Error%20while%20parsing%20URL%2C%20see%20console.%22)%3B%0D%0A%20%20%7D%0D%0A%7D%0D%0A%0D%0A%2F**%0D%0A%20*%0D%0A%20*%20%40param%20%7BURL%7D%20url%0D%0A%20*%2F%0D%0Aasync%20function%20parsePost(url)%20%7B%0D%0A%20%20%2F%2F%20remove%20search%20query%0D%0A%20%20url.search%20%3D%20%22%22%3B%0D%0A%0D%0A%20%20let%20path%20%3D%20url.pathname.split(%22%2F%22)%3B%0D%0A%0D%0A%20%20if%20(path%5B1%5D%20!%3D%3D%20%22posts%22)%20%7B%0D%0A%20%20%20%20alert(%22This%20script%20can%20only%20handle%20posts.%22)%3B%0D%0A%20%20%20%20return%3B%20%2F%2F%20ends%20script%0D%0A%20%20%7D%0D%0A%0D%0A%20%20const%20image%20%3D%20%7B%0D%0A%20%20%20%20url%3A%20%22%22%2C%0D%0A%20%20%20%20artists%3A%20%5B%5D%2C%0D%0A%20%20%7D%3B%0D%0A%0D%0A%20%20image.url%20%3D%20document.querySelector(%22%23image%22).src%3B%0D%0A%0D%0A%20%20const%20artistTagList%20%3D%20document.querySelectorAll(%22.artist-tag-list%20%3E%20li%22)%3B%0D%0A%0D%0A%20%20for%20(const%20li%20of%20artistTagList)%20%7B%0D%0A%20%20%20%20const%20name%20%3D%20li.querySelector(%22.search-tag%22).text%3B%0D%0A%0D%0A%20%20%20%20%2F%2F%20Aborts%20on%20conditional%20dnp%2C%20comment%20to%20disable%0D%0A%20%20%20%20if%20(name%20%3D%3D%3D%20%22conditional%20dnp%22)%20%7B%0D%0A%20%20%20%20%20%20return%20%22Conditional%20do%20not%20post%20detected.%20Please%20check%20if%20you%20are%20allowed%20to%20share%20this%20post.%20Manual%20work%20required.%22%3B%0D%0A%20%20%20%20%7D%0D%0A%0D%0A%20%20%20%20const%20sources%20%3D%20await%20parseArtist(%0D%0A%20%20%20%20%20%20new%20URL(li.querySelector(%22.wiki-link%22).href)%0D%0A%20%20%20%20)%3B%0D%0A%20%20%20%20image.artists.push(%7B%0D%0A%20%20%20%20%20%20name%3A%20name%2C%0D%0A%20%20%20%20%20%20sources%3A%20sources%2C%0D%0A%20%20%20%20%7D)%3B%0D%0A%20%20%7D%0D%0A%0D%0A%20%20let%20text%20%3D%20%60!%5B%5D(%24%7Bimage.url%7D)%20%20%5Cnby%60%3B%0D%0A%0D%0A%20%20for%20(const%20i%20in%20image.artists)%20%7B%0D%0A%20%20%20%20if%20(i%20%3E%200)%20%7B%0D%0A%20%20%20%20%20%20text%20%2B%3D%20%22%20and%22%3B%0D%0A%20%20%20%20%7D%0D%0A%20%20%20%20text%20%2B%3D%20%60%20%24%7Bimage.artists%5Bi%5D.name%7D%60%3B%0D%0A%20%20%20%20for%20(const%20y%20in%20image.artists%5Bi%5D.sources)%20%7B%0D%0A%20%20%20%20%20%20if%20(y%20%3C%201)%20%7B%0D%0A%20%20%20%20%20%20%20%20text%20%2B%3D%20%22%20%22%3B%0D%0A%20%20%20%20%20%20%7D%0D%0A%20%20%20%20%20%20text%20%2B%3D%20%60%5B%5B%24%7BNumber(y)%20%2B%201%7D%5D(%24%7Bimage.artists%5Bi%5D.sources%5By%5D%7D)%5D%60%3B%0D%0A%20%20%20%20%7D%0D%0A%20%20%7D%0D%0A%20%20text%20%2B%3D%20%22.%22%3B%0D%0A%0D%0A%20%20return%20text%3B%0D%0A%7D%0D%0A%0D%0A%2F**%0D%0A%20*%0D%0A%20*%20%40param%20%7BURL%7D%20url%0D%0A%20*%2F%0D%0Aasync%20function%20parseArtist(url)%20%7B%0D%0A%20%20%2F%2F%20relative%20url%20has%20to%20be%20fixed%0D%0A%20%20const%20correctURL%20%3D%20new%20URL(url.pathname%2C%20%22https%3A%2F%2Fe621.net%22)%3B%0D%0A%20%20correctURL.search%20%3D%20url.search%3B%0D%0A%20%20const%20response%20%3D%20await%20fetch(correctURL)%3B%0D%0A%20%20const%20html%20%3D%20await%20response.text()%3B%0D%0A%0D%0A%20%20const%20parser%20%3D%20new%20DOMParser()%3B%0D%0A%20%20const%20eDoc%20%3D%20parser.parseFromString(html%2C%20%22text%2Fhtml%22)%3B%0D%0A%0D%0A%20%20const%20res%20%3D%20eDoc.evaluate(%0D%0A%20%20%20%20%22%2F%2F*%5B%40id%3D'c-artists'%5D%2Fdiv%2Fdiv%2Ful%2Fli%5Bstrong%2Ftext()%3D'URLs'%5D%2Ffollowing-sibling%3A%3Aul%2Fli%2Fa%22%2C%0D%0A%20%20%20%20eDoc%0D%0A%20%20)%3B%0D%0A%0D%0A%20%20const%20sources%20%3D%20%5B%5D%3B%0D%0A%0D%0A%20%20let%20n%3B%0D%0A%20%20while%20((n%20%3D%20res.iterateNext()))%20%7B%0D%0A%20%20%20%20sources.push(n.href)%3B%0D%0A%20%20%7D%0D%0A%0D%0A%20%20return%20sources%3B%0D%0A%7D%0D%0A%0D%0Afunction%20prepareOutput()%20%7B%0D%0A%20%20const%20imageContainer%20%3D%20document.querySelector(%22%23image-container%22)%3B%0D%0A%20%20const%20copyDiv%20%3D%20document.createElement(%22div%22)%3B%0D%0A%20%20copyDiv.id%20%3D%20%22to-md%22%3B%0D%0A%20%20copyDiv.classList.add(%22comment%22%2C%20%22comment-post-grid%22)%3B%0D%0A%20%20copyDiv.style.marginBottom%20%3D%20%221em%22%3B%0D%0A%0D%0A%20%20copyDiv.innerHTML%20%3D%20%60%0D%0A%20%20%20%20%3Cdiv%20class%3D%22author-info%22%3E%0D%0A%20%20%20%20%20%20%3Cdiv%20class%3D%22name-rank%22%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%3Ch4%20class%3D%22author-name%22%3E%3Ca%20href%3D%22https%3A%2F%2Fyiffit.net%2Fu%2Fpurringfox%22%20target%3D%22_blank%22%20class%3D%22user-member%20with-style%22%3EPurringFox%3C%2Fa%3E%3C%2Fh4%3E%0D%0A%20%20%20%20%20%20%20%20%20%20Hackerman%0D%0A%20%20%20%20%20%20%20%20%3C%2Fdiv%3E%0D%0A%20%20%20%20%20%20%20%20%3Cdiv%20class%3D%22avatar%22%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%3Cdiv%20class%3D%22post-thumbnail%22%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Ca%20href%3D%22https%3A%2F%2Fe621.net%2Fposts%2F2294957%22%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cimg%20class%3D%22post-thumbnail-img%22%20src%3D%22https%3A%2F%2Fstatic1.e621.net%2Fdata%2Fpreview%2F64%2Fa2%2F64a24eba91e5c666cf1ed34833fa86ab.jpg%22%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%3C%2Fa%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%3C%2Fdiv%3E%0D%0A%20%20%20%20%20%20%20%20%3C%2Fdiv%3E%0D%0A%20%20%20%20%3C%2Fdiv%3E%0D%0A%20%20%20%20%3Cdiv%20class%3D%22content%22%3E%0D%0A%20%20%20%20%20%20%3Cdiv%20class%3D%22body%20dtext-container%22%3E%0D%0A%20%20%20%20%20%20%20%20%3Cdiv%20class%3D%22styled-dtext%22%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%3Cpre%20id%3D%22md-text%22%3Eloading...%3C%2Fpre%3E%0D%0A%20%20%20%20%20%20%20%20%3C%2Fdiv%3E%0D%0A%20%20%20%20%20%20%3C%2Fdiv%3E%0D%0A%20%20%20%20%20%20%20%20%3Cdiv%20class%3D%22content-menu%22%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%3Cmenu%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cli%3E%3Ca%20id%3D%22copy-md-link%22%20href%3D%22%23copy-md%22%20class%3D%22reply-link%20comment-reply-link%22%20style%3D%22visibility%3A%20hidden%3B%22%3ECopy%3C%2Fa%3E%3C%2Fli%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%3C%2Fmenu%3E%0D%0A%20%20%20%20%20%20%3C%2Fdiv%3E%0D%0A%20%20%20%20%3C%2Fdiv%3E%0D%0A%20%20%20%20%60%3B%0D%0A%0D%0A%20%20imageContainer.parentElement.insertBefore(copyDiv%2C%20imageContainer)%3B%0D%0A%7D%0D%0A%0D%0Afunction%20insertMD(text)%20%7B%0D%0A%20%20document.querySelector(%22%23md-text%22).innerHTML%20%3D%20text%3B%0D%0A%0D%0A%20%20const%20link%20%3D%20document.querySelector(%22%23copy-md-link%22)%3B%0D%0A%20%20link.style.visibility%20%3D%20%22visible%22%3B%0D%0A%20%20link.addEventListener(%22click%22%2C%20(e)%20%3D%3E%20toClipboard(e%2C%20text))%3B%0D%0A%7D%0D%0A%0D%0A%2F**%0D%0A%20*%0D%0A%20*%20%40param%20%7BEvent%7D%20e%0D%0A%20*%2F%0D%0Afunction%20toClipboard(e%2C%20text)%20%7B%0D%0A%20%20e.preventDefault()%3B%0D%0A%20%20navigator.clipboard.writeText(text)%3B%0D%0A%20%20e.target.style.color%20%3D%20%22var(--color-score-positive)%22%3B%0D%0A%20%20setTimeout(()%20%3D%3E%20%7B%0D%0A%20%20%20%20e.target.style.color%20%3D%20%22%22%3B%0D%0A%20%20%7D%2C%203000)%3B%0D%0A%7D%3Brun()%3B%7D)()%3B

To install just create a new bookmark, name as you want it, and past the above as url. Be sure to copy the whole content. Starts with “javascript” and ends with “%3B”.

To use it, just open the e621 post and press the bookmark.

The bookmarklet has to be encode with encodeURIComponent that’s why it looks like gibberisch, you can view below the code before it is encoded.

Source code
async function run() {
  let location = window.location;
  try {
    url = new URL(location);
    if (url.hostname === "e621.net" || url.hostname === "www.e621.net") {
      prepareOutput();
      const text = await parsePost(url);
      insertMD(text);
    } else {
      alert("This script only works with e621.");
    }
  } catch (error) {
    alert("Error while parsing URL, see console.");
  }
}

/**
 *
 * @param {URL} url
 */
async function parsePost(url) {
  // remove search query
  url.search = "";

  let path = url.pathname.split("/");

  if (path[1] !== "posts") {
    alert("This script can only handle posts.");
    return; // ends script
  }

  const image = {
    url: "",
    artists: [],
  };

  image.url = document.querySelector("#image").src;

  const artistTagList = document.querySelectorAll(".artist-tag-list > li");

  for (const li of artistTagList) {
    const name = li.querySelector(".search-tag").text;

    // Aborts on conditional dnp, comment to disable
    if (name === "conditional dnp") {
      return "Conditional do not post detected. Please check if you are allowed to share this post. Manual work required.";
    }

    const sources = await parseArtist(
      new URL(li.querySelector(".wiki-link").href)
    );
    image.artists.push({
      name: name,
      sources: sources,
    });
  }

  let text = `![](${image.url})  \nby`;

  for (const i in image.artists) {
    if (i > 0) {
      text += " and";
    }
    text += ` ${image.artists[i].name}`;
    for (const y in image.artists[i].sources) {
      if (y < 1) {
        text += " ";
      }
      text += `[[${Number(y) + 1}](${image.artists[i].sources[y]})]`;
    }
  }
  text += ".";

  return text;
}

/**
 *
 * @param {URL} url
 */
async function parseArtist(url) {
  // relative url has to be fixed
  const correctURL = new URL(url.pathname, "https://e621.net");
  correctURL.search = url.search;
  const response = await fetch(correctURL);
  const html = await response.text();

  const parser = new DOMParser();
  const eDoc = parser.parseFromString(html, "text/html");

  const res = eDoc.evaluate(
    "//*[@id='c-artists']/div/div/ul/li[strong/text()='URLs']/following-sibling::ul/li/a",
    eDoc
  );

  const sources = [];

  let n;
  while ((n = res.iterateNext())) {
    sources.push(n.href);
  }

  return sources;
}

function prepareOutput() {
  const imageContainer = document.querySelector("#image-container");
  const copyDiv = document.createElement("div");
  copyDiv.id = "to-md";
  copyDiv.classList.add("comment", "comment-post-grid");
  copyDiv.style.marginBottom = "1em";

  copyDiv.innerHTML = `
    <div class="author-info">
      <div class="name-rank">
          <h4 class="author-name"><a href="https://yiffit.net/u/purringfox" target="_blank" class="user-member with-style">PurringFox</a></h4>
          Hackerman
        </div>
        <div class="avatar">
          <div class="post-thumbnail">
            <a href="https://e621.net/posts/2294957">
              <img class="post-thumbnail-img" src="https://static1.e621.net/data/preview/64/a2/64a24eba91e5c666cf1ed34833fa86ab.jpg">
            </a>
          </div>
        </div>
    </div>
    <div class="content">
      <div class="body dtext-container">
        <div class="styled-dtext">
          <pre id="md-text">loading...</pre>
        </div>
      </div>
        <div class="content-menu">
          <menu>
              <li><a id="copy-md-link" href="#copy-md" class="reply-link comment-reply-link" style="visibility: hidden;">Copy</a></li>
          </menu>
      </div>
    </div>
    `;

  imageContainer.parentElement.insertBefore(copyDiv, imageContainer);
}

function insertMD(text) {
  document.querySelector("#md-text").innerHTML = text;

  const link = document.querySelector("#copy-md-link");
  link.style.visibility = "visible";
  link.addEventListener("click", (e) => toClipboard(e, text));
}

/**
 *
 * @param {Event} e
 */
function toClipboard(e, text) {
  e.preventDefault();
  navigator.clipboard.writeText(text);
  e.target.style.color = "var(--color-score-positive)";
  setTimeout(() => {
    e.target.style.color = "";
  }, 3000);
}

If you find bugs, you can post them here. I might have a look at them. might.

Post in example video by hecatta [1][2].

Profile picture in example video by redcrystal.

  • Wander@yiffit.netM
    link
    fedilink
    English
    arrow-up
    5
    ·
    edit-2
    1 year ago

    Test:


    by fffox [1][2].

    Edit, by the way, good choice in loading only e621 sources. A potential vulnerability could have come from loading images from external non-trusted sources, but from checking the code and decoding the bookmarklet encoding everything checks out! Good work <3

  • Wander@yiffit.netM
    link
    fedilink
    English
    arrow-up
    4
    ·
    1 year ago

    Hello! Thank you a lot for this ^^

    I’ll give it a try in a moment. But before I do, can I propose something to you?

    I noticed that I’m able to insert an arbitrary html header into lemmy-ui as part of the environment variables. This could include a <script src=“”> type of header linking to a javascript file of our choice which I could host myself so that there’s no problems with same-origin policies.

    I have already started working on a script that creates a “unblur nsfw content” setting in the user’s settings and stores the value in the browser’s localstorage. This is just an example of the type of customization that could be done.

    If you ever come up with any interesting “user scripts” for the lemmy ui, I might be able to include them into the site itself. I’m using greasemonkey for the development since it loads automatically every time you open a page and you can also do url filtering, but the idea is to insert the end scripts into the page itself (after publishing them for peer review).

    So, if you ever come up with an interesting script idea, feel free to let me know or send me the result. You might also be interested in this lemmy communty: !plugins@sh.itjust.works

    • PurringFox@yiffit.netOP
      link
      fedilink
      English
      arrow-up
      1
      ·
      1 year ago

      At first I actually wanted to create an additional button for the editor in create post, to paste the link to, but then I decided against it, since I wanted a more universal approach.

      But now I’m actually interested in writing lemmy UI plugins, maybe I’ll rewrite my code :).

      First I was confused but now I think I get it. You already did something on here, that nsfw stuff is unblurred by default (since it is at least like that for me now), but you would like to make that toggleable. What talks against the “disableblur” and “shownsfw” themes that are selectable. Are these also experiments from you?

      I’ll do some research regarding lemmy ui now.
      It would actually be better, when there already would be a way to extend behavior server side, like modifying the HTML that gets served, instead of doing it ad-hoc on client side (performance). Also if at point there are to many scripts “patched in” they’ll probably start to be incompatible. Example, I append a child node to a element called “x” and another script removes the element “x” for some reason, which would break mine. Let’s see if extensions are planned in lemmy. Else forking the entire UI and expanding on that would also be a (but more time intensive) possibility.

      I’ll come back to it, after I did some more research.

      • Wander@yiffit.netM
        link
        fedilink
        English
        arrow-up
        1
        ·
        1 year ago

        Right now the blur is controlled via css. Disableblur is a theme that is set by default and which incorporates the css classes that override the default behavior. The problem is that blur is enabled again when a user chooses a different theme.

        What I was thinking of doing was having a script that would fetch your preference from local storage and then dynamically change CSS. I agree to keep it light, but there’s a lot of potential. Doing things serverside is possible, but I’d have to learn how to compile and dockerize from source, which might be complicated because the site is written in inferno and would also break future updates.

        • PurringFox@yiffit.netOP
          link
          fedilink
          English
          arrow-up
          2
          ·
          1 year ago

          I’m researching right now. Regarding the blurring there is already a pull request, that implements the exact feature you would like to do (if I understand you correctly) https://github.com/LemmyNet/lemmy-ui/pull/1640.

          I agree, maintaining a fork of lemmy-ui and rebuilding it every change is not feasible. Sadly it looks like there are not much more possibilities of extending the ui than the custom header and custom theme css.

          So I think the header scripts it is… But one thing I think would be nice is, that these scripts can be enabled/disabled by the users individually and should be off by default. So that people not interested in that are not impacted by possible performance issues or bugs. Nice would be if it would be possible to create an additional settings tab, where all these extra scripts are toggleable.

          Let’s see, if I have time I’ll setup a dev-environment to experiment with stuff.

          • Wander@yiffit.netM
            link
            fedilink
            English
            arrow-up
            1
            ·
            1 year ago

            Thank you <3 There’s no rush, although I’m pretty excited about the potential!