laxamentumtech / audnexus

An audiobook data aggregation API that harmonizes data from multiple sources into a unified stream. It offers a consistent and user-friendly source of audiobook data for various applications.
https://audnex.us/
GNU General Public License v3.0
115 stars 5 forks source link

Square author photos #530

Closed csandman closed 1 year ago

csandman commented 1 year ago

One feature that could be nice to add to this package, would be returning a square version of an author's photo in addition to the full sized one. The main benefit I can see for this is the Plex agent that was written for this package. When a non-square image is used for an artist photo in Plex, it is always cropped at the center, whereas by default the Audible author images are centered on their faces. And because there are no standards for the shape of the Audible author images, they always look inconsistent in Plex.

Plex:

image

Audible:

image

I assume Audible locates the face using Amazon Rekognition or something of the sort.

By default, the Audible author images are only thumbnails, which are obviously too low in resolution to be useful. However, if you know the dimensions of the raw image you generate a square image from, it's possible to generate a facially centered square image by modifying the initial image URL.

By default, the Author images come in the following format:

https://images-na.ssl-images-amazon.com/images/I/81iHT1SI1lL.__01_SX120_CR0,0,120,120__.jpg

image

And from looking at your code, I know you remove the __01_SX120_CR0,0,120,120__. part to get the URL for the full sized image, which looks something like this:

https://images-na.ssl-images-amazon.com/images/I/81iHT1SI1lL.jpg

image

Now, I'm not sure if you've tried messing with their initial URL format, but if you just increase all the numbers to some arbitrarily high resolution number, borders will be added to fill in the extra space if the image isn't large enough to accommodate:

https://images-na.ssl-images-amazon.com/images/I/81iHT1SI1lL.__01_SX1500_CR0,0,1500,1500__.jpg

image

However, I have found that if you pull in the image and find the smaller dimension (usually the width but sometimes the photos are in landscape), and replace all the 120 values in the original thumbnail URL with that number, you'll get the largest image size available while still being a square image centered on the author's face. Taking this image for example, the width of the full size image is 464 (still pretty small but much bigger than 120), and the modified original URL would look like this:

https://images-na.ssl-images-amazon.com/images/I/81iHT1SI1lL.__01_SX464_CR0,0,464,464__.jpg

image

Which is much more useful for something like Plex! The best part is, you don't have to do any subject identification on the photo itself because Amazon has already done the work for you. Plus, this whole process can be scripted pretty easily in Node:

import sizeOf from 'image-size';
import fetch from 'node-fetch';

const AUTHOR_IMAGE_URL =
  'https://images-na.ssl-images-amazon.com/images/I/81iHT1SI1lL.__01_SX120_CR0,0,120,120__.jpg';

const getSquareImage = async (url) => {
  const fullAuthorUrl = url.replace('__01_SX120_CR0,0,120,120__.', '');

  const imageRes = await fetch(fullAuthorUrl);

  const imgBuffer = await imageRes.buffer();

  const imageSize = await sizeOf(imgBuffer);

  const minDimension = Math.min(imageSize.width, imageSize.height);

  const largeSquareImgUrl = url.replace(/120/g, minDimension);
  console.log(largeSquareImgUrl);

  return largeSquareImgUrl;
};

getSquareImage(AUTHOR_IMAGE_URL);

Idk if this is outside the scope of what you intend to deliver with this project, but it's something worth thinking about! I think it would be a great alternative to using the full size image for Plex thumbnails.

image

The only issue with this process is that you'd have to load a lot of images into memory to check their dimensions, which would probably affect processing time/power. I'm not sure how that would ultimately affect the performance of this tool, but I think it's worth investigating.

djdembeck commented 1 year ago

Sorry for the delayed response (I've had some life changes to adjust to).

You put a lot of thought into this, and I love that! Have you run any quick benchmarking on how long it takes to process an image vs. not processing?

csandman commented 1 year ago

No problem, this wasn't a super urgent issue haha! I have not, but I can run some tests and get back to you!

csandman commented 1 year ago

Well I gave benchmarking it a shot, and found the function to execute pretty quickly, but I'd have to assume it would be dependent on the internet connection of the server the app is hosted on. My download speed is pretty good, and all the functions ran pretty fast.

Here was my test setup:

import sizeOf from 'image-size';
import fetch from 'node-fetch';
import { performance } from 'perf_hooks';

const AUTHOR_IMAGE_URLS = [
  'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/dcqug62o4s52ubd61ogont4t3l.__01_SX120_CR0,0,120,120__.jpg',
  'https://images-na.ssl-images-amazon.com/images/I/81iHT1SI1lL.__01_SX120_CR0,0,120,120__.jpg',
  'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/h7fqg9j573mbdr7c8vvuungat2.__01_SX120_CR0,0,120,120__.jpg',
  'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/k9gtp50b3kj06bikpg230de34c.__01_SX120_CR0,0,120,120__.jpg',
  'https://images-na.ssl-images-amazon.com/images/I/21pWMFYIf1L.__01_SX120_CR0,0,120,120__.jpg',
  'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/bsf846hh2eo2dse1t05c5rmq2e.__01_SX120_CR0,0,120,120__.jpg',
  'https://images-na.ssl-images-amazon.com/images/I/A1QQgrp3VsL.__01_SX120_CR0,0,120,120__.jpg',
  'https://images-na.ssl-images-amazon.com/images/I/51Uaz1il9IL.__01_SX120_CR0,0,120,120__.jpg',
  'https://images-na.ssl-images-amazon.com/images/I/41prWGgXhIL.__01_SX120_CR0,0,120,120__.jpg',
  'https://images-na.ssl-images-amazon.com/images/I/312hyoBaIjL.__01_SX120_CR0,0,120,120__.jpg',
  'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/i1pi9ns4smtfi8o49duan1j2h4.__01_SX120_CR0,0,120,120__.jpg',
  'https://images-na.ssl-images-amazon.com/images/I/9132UvcXnGL.__01_SX120_CR0,0,120,120__.jpg',
  'https://images-na.ssl-images-amazon.com/images/I/313l0DRU9VL.__01_SX120_CR0,0,120,120__.jpg',
  'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/1u8p8qq9fr3eq2ocgma6j56s0g.__01_SX120_CR0,0,120,120__.jpg',
  'https://images-na.ssl-images-amazon.com/images/I/21-OqMcKsAL.__01_SX120_CR0,0,120,120__.jpg',
  'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/8cigckin175jtpsk3gs361r4ss.__01_SX120_CR0,0,120,120__.jpg',
  'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/990aat17klqmn21r86uvo8lb45.__01_SX120_CR0,0,120,120__.jpg',
  'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/nn5mlv5h2vfcdbnmm90dtb6ltm.__01_SX120_CR0,0,120,120__.jpg',
  'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/u27aie1p3kirmbckb22b7lo63u.__01_SX120_CR0,0,120,120__.jpg',
  'https://images-na.ssl-images-amazon.com/images/I/51TEICxU-CL.__01_SX120_CR0,0,120,120__.jpg',
  'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/h7u6f9a3e680k4dver07t4s0t2.__01_SX120_CR0,0,120,120__.jpg',
  'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/ndtsjs1mbus8ubg04hv02g5f3l.__01_SX120_CR0,0,120,120__.jpg',
  'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/ppppnrtb11dqtjikkq5carfg90.__01_SX120_CR0,0,120,120__.jpg',
  'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/iuibkacc38lubdr0km2akilhe1.__01_SX120_CR0,0,120,120__.jpg',
  'https://images-na.ssl-images-amazon.com/images/I/21dJC-mHlYL.__01_SX120_CR0,0,120,120__.jpg',
  'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/kps6s9higps8qfj9iaghq872n0.__01_SX120_CR0,0,120,120__.jpg',
  'https://images-na.ssl-images-amazon.com/images/I/71AcCRBHBML.__01_SX120_CR0,0,120,120__.jpg',
  'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/96pn5d84f0chcg248pgrp5hel9.__01_SX120_CR0,0,120,120__.jpg',
  'https://images-na.ssl-images-amazon.com/images/I/415Xnu0pwBL.__01_SX120_CR0,0,120,120__.jpg',
  'https://images-na.ssl-images-amazon.com/images/I/61w3pqVMCZL.__01_SX120_CR0,0,120,120__.jpg',
  'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/u65m852f5j96v6ud70hpfb3hu7.__01_SX120_CR0,0,120,120__.jpg',
  'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/rn4hchq6ouadmsu04f1qjs0j3k.__01_SX120_CR0,0,120,120__.jpg',
  'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/fkeglaqq0pic05a0v6ieqt4iv5.__01_SX120_CR0,0,120,120__.jpg',
  'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/11q81821d0ovjemedg11f7knb8.__01_SX120_CR0,0,120,120__.jpg',
  'https://images-na.ssl-images-amazon.com/images/I/B1PNLDB9k9S.__01_SX120_CR0,0,120,120__.jpg',
  'https://images-na.ssl-images-amazon.com/images/I/51rgIda7HDL.__01_SX120_CR0,0,120,120__.jpg',
];

const getSquareImage = async (url) => {
  const start = performance.now();

  const fullAuthorUrl = url.replace('__01_SX120_CR0,0,120,120__.', '');
  const imageRes = await fetch(fullAuthorUrl);
  const imgBuffer = await imageRes.buffer();
  const imageSize = sizeOf(imgBuffer);
  const minDimension = Math.min(imageSize.width, imageSize.height);
  const largeSquareImgUrl = url.replace(/120/g, minDimension);

  const end = performance.now();

  return {
    original: url,
    full: fullAuthorUrl,
    square: largeSquareImgUrl,
    time: end - start,
  };
};

const getAll = async () => {
  const results = await Promise.all(
    AUTHOR_IMAGE_URLS.map((url) => getSquareImage(url))
  );

  console.log(results);

  const avgTime =
    results.reduce((acc, curr) => acc + curr.time, 0) / results.length;

  console.log('average time:', avgTime);
  console.log(`for ${results.length} results`);
};

getAll();

And these were my results:

[
  {
    original: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/dcqug62o4s52ubd61ogont4t3l.__01_SX120_CR0,0,120,120__.jpg',
    full: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/dcqug62o4s52ubd61ogont4t3l.jpg',
    square: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/dcqug62o4s52ubd61ogont4t3l.__01_SX1181_CR0,0,1181,1181__.jpg',
    time: 139.79129207134247
  },
  {
    original: 'https://images-na.ssl-images-amazon.com/images/I/81iHT1SI1lL.__01_SX120_CR0,0,120,120__.jpg',
    full: 'https://images-na.ssl-images-amazon.com/images/I/81iHT1SI1lL.jpg',
    square: 'https://images-na.ssl-images-amazon.com/images/I/81iHT1SI1lL.__01_SX464_CR0,0,464,464__.jpg',
    time: 138.65283298492432
  },
  {
    original: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/h7fqg9j573mbdr7c8vvuungat2.__01_SX120_CR0,0,120,120__.jpg',
    full: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/h7fqg9j573mbdr7c8vvuungat2.jpg',
    square: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/h7fqg9j573mbdr7c8vvuungat2.__01_SX328_CR0,0,328,328__.jpg',
    time: 77.4843339920044
  },
  {
    original: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/k9gtp50b3kj06bikpg230de34c.__01_SX120_CR0,0,120,120__.jpg',
    full: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/k9gtp50b3kj06bikpg230de34c.jpg',
    square: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/k9gtp50b3kj06bikpg230de34c.__01_SX757_CR0,0,757,757__.jpg',
    time: 110.3434579372406
  },
  {
    original: 'https://images-na.ssl-images-amazon.com/images/I/21pWMFYIf1L.__01_SX120_CR0,0,120,120__.jpg',
    full: 'https://images-na.ssl-images-amazon.com/images/I/21pWMFYIf1L.jpg',
    square: 'https://images-na.ssl-images-amazon.com/images/I/21pWMFYIf1L.__01_SX229_CR0,0,229,229__.jpg',
    time: 77.70391607284546
  },
  {
    original: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/bsf846hh2eo2dse1t05c5rmq2e.__01_SX120_CR0,0,120,120__.jpg',
    full: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/bsf846hh2eo2dse1t05c5rmq2e.jpg',
    square: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/bsf846hh2eo2dse1t05c5rmq2e.__01_SX574_CR0,0,574,574__.jpg',
    time: 95.5725839138031
  },
  {
    original: 'https://images-na.ssl-images-amazon.com/images/I/A1QQgrp3VsL.__01_SX120_CR0,0,120,120__.jpg',
    full: 'https://images-na.ssl-images-amazon.com/images/I/A1QQgrp3VsL.jpg',
    square: 'https://images-na.ssl-images-amazon.com/images/I/A1QQgrp3VsL.__01_SX1260_CR0,0,1260,1260__.jpg',
    time: 145.1996660232544
  },
  {
    original: 'https://images-na.ssl-images-amazon.com/images/I/51Uaz1il9IL.__01_SX120_CR0,0,120,120__.jpg',
    full: 'https://images-na.ssl-images-amazon.com/images/I/51Uaz1il9IL.jpg',
    square: 'https://images-na.ssl-images-amazon.com/images/I/51Uaz1il9IL.__01_SX427_CR0,0,427,427__.jpg',
    time: 78.88208293914795
  },
  {
    original: 'https://images-na.ssl-images-amazon.com/images/I/41prWGgXhIL.__01_SX120_CR0,0,120,120__.jpg',
    full: 'https://images-na.ssl-images-amazon.com/images/I/41prWGgXhIL.jpg',
    square: 'https://images-na.ssl-images-amazon.com/images/I/41prWGgXhIL.__01_SX160_CR0,0,160,160__.jpg',
    time: 83.80075001716614
  },
  {
    original: 'https://images-na.ssl-images-amazon.com/images/I/312hyoBaIjL.__01_SX120_CR0,0,120,120__.jpg',
    full: 'https://images-na.ssl-images-amazon.com/images/I/312hyoBaIjL.jpg',
    square: 'https://images-na.ssl-images-amazon.com/images/I/312hyoBaIjL.__01_SX207_CR0,0,207,207__.jpg',
    time: 81.03462505340576
  },
  {
    original: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/i1pi9ns4smtfi8o49duan1j2h4.__01_SX120_CR0,0,120,120__.jpg',
    full: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/i1pi9ns4smtfi8o49duan1j2h4.jpg',
    square: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/i1pi9ns4smtfi8o49duan1j2h4.__01_SX227_CR0,0,227,227__.jpg',
    time: 85.4155410528183
  },
  {
    original: 'https://images-na.ssl-images-amazon.com/images/I/9132UvcXnGL.__01_SX120_CR0,0,120,120__.jpg',
    full: 'https://images-na.ssl-images-amazon.com/images/I/9132UvcXnGL.jpg',
    square: 'https://images-na.ssl-images-amazon.com/images/I/9132UvcXnGL.__01_SX1605_CR0,0,1605,1605__.jpg',
    time: 126.22391700744629
  },
  {
    original: 'https://images-na.ssl-images-amazon.com/images/I/313l0DRU9VL.__01_SX120_CR0,0,120,120__.jpg',
    full: 'https://images-na.ssl-images-amazon.com/images/I/313l0DRU9VL.jpg',
    square: 'https://images-na.ssl-images-amazon.com/images/I/313l0DRU9VL.__01_SX235_CR0,0,235,235__.jpg',
    time: 87.43112504482269
  },
  {
    original: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/1u8p8qq9fr3eq2ocgma6j56s0g.__01_SX120_CR0,0,120,120__.jpg',
    full: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/1u8p8qq9fr3eq2ocgma6j56s0g.jpg',
    square: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/1u8p8qq9fr3eq2ocgma6j56s0g.__01_SX208_CR0,0,208,208__.jpg',
    time: 86.06299996376038
  },
  {
    original: 'https://images-na.ssl-images-amazon.com/images/I/21-OqMcKsAL.__01_SX120_CR0,0,120,120__.jpg',
    full: 'https://images-na.ssl-images-amazon.com/images/I/21-OqMcKsAL.jpg',
    square: 'https://images-na.ssl-images-amazon.com/images/I/21-OqMcKsAL.__01_SX149_CR0,0,149,149__.jpg',
    time: 89.55879199504852
  },
  {
    original: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/8cigckin175jtpsk3gs361r4ss.__01_SX120_CR0,0,120,120__.jpg',
    full: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/8cigckin175jtpsk3gs361r4ss.jpg',
    square: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/8cigckin175jtpsk3gs361r4ss.__01_SX957_CR0,0,957,957__.jpg',
    time: 94.46991693973541
  },
  {
    original: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/990aat17klqmn21r86uvo8lb45.__01_SX120_CR0,0,120,120__.jpg',
    full: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/990aat17klqmn21r86uvo8lb45.jpg',
    square: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/990aat17klqmn21r86uvo8lb45.__01_SX512_CR0,0,512,512__.jpg',
    time: 146.34120893478394
  },
  {
    original: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/nn5mlv5h2vfcdbnmm90dtb6ltm.__01_SX120_CR0,0,120,120__.jpg',
    full: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/nn5mlv5h2vfcdbnmm90dtb6ltm.jpg',
    square: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/nn5mlv5h2vfcdbnmm90dtb6ltm.__01_SX1880_CR0,0,1880,1880__.jpg',
    time: 183.3122079372406
  },
  {
    original: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/u27aie1p3kirmbckb22b7lo63u.__01_SX120_CR0,0,120,120__.jpg',
    full: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/u27aie1p3kirmbckb22b7lo63u.jpg',
    square: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/u27aie1p3kirmbckb22b7lo63u.__01_SX260_CR0,0,260,260__.jpg',
    time: 94.17554199695587
  },
  {
    original: 'https://images-na.ssl-images-amazon.com/images/I/51TEICxU-CL.__01_SX120_CR0,0,120,120__.jpg',
    full: 'https://images-na.ssl-images-amazon.com/images/I/51TEICxU-CL.jpg',
    square: 'https://images-na.ssl-images-amazon.com/images/I/51TEICxU-CL.__01_SX229_CR0,0,229,229__.jpg',
    time: 112.25995790958405
  },
  {
    original: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/h7u6f9a3e680k4dver07t4s0t2.__01_SX120_CR0,0,120,120__.jpg',
    full: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/h7u6f9a3e680k4dver07t4s0t2.jpg',
    square: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/h7u6f9a3e680k4dver07t4s0t2.__01_SX2000_CR0,0,2000,2000__.jpg',
    time: 269.72129106521606
  },
  {
    original: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/ndtsjs1mbus8ubg04hv02g5f3l.__01_SX120_CR0,0,120,120__.jpg',
    full: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/ndtsjs1mbus8ubg04hv02g5f3l.jpg',
    square: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/ndtsjs1mbus8ubg04hv02g5f3l.__01_SX500_CR0,0,500,500__.jpg',
    time: 109.27412497997284
  },
  {
    original: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/ppppnrtb11dqtjikkq5carfg90.__01_SX120_CR0,0,120,120__.jpg',
    full: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/ppppnrtb11dqtjikkq5carfg90.jpg',
    square: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/ppppnrtb11dqtjikkq5carfg90.__01_SX648_CR0,0,648,648__.jpg',
    time: 138.23641693592072
  },
  {
    original: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/iuibkacc38lubdr0km2akilhe1.__01_SX120_CR0,0,120,120__.jpg',
    full: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/iuibkacc38lubdr0km2akilhe1.jpg',
    square: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/iuibkacc38lubdr0km2akilhe1.__01_SX2736_CR0,0,2736,2736__.jpg',
    time: 177.8967500925064
  },
  {
    original: 'https://images-na.ssl-images-amazon.com/images/I/21dJC-mHlYL.__01_SX120_CR0,0,120,120__.jpg',
    full: 'https://images-na.ssl-images-amazon.com/images/I/21dJC-mHlYL.jpg',
    square: 'https://images-na.ssl-images-amazon.com/images/I/21dJC-mHlYL.__01_SX200_CR0,0,200,200__.jpg',
    time: 117.37929105758667
  },
  {
    original: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/kps6s9higps8qfj9iaghq872n0.__01_SX120_CR0,0,120,120__.jpg',
    full: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/kps6s9higps8qfj9iaghq872n0.jpg',
    square: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/kps6s9higps8qfj9iaghq872n0.__01_SX300_CR0,0,300,300__.jpg',
    time: 122.8955830335617
  },
  {
    original: 'https://images-na.ssl-images-amazon.com/images/I/71AcCRBHBML.__01_SX120_CR0,0,120,120__.jpg',
    full: 'https://images-na.ssl-images-amazon.com/images/I/71AcCRBHBML.jpg',
    square: 'https://images-na.ssl-images-amazon.com/images/I/71AcCRBHBML.__01_SX900_CR0,0,900,900__.jpg',
    time: 135.1004580259323
  },
  {
    original: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/96pn5d84f0chcg248pgrp5hel9.__01_SX120_CR0,0,120,120__.jpg',
    full: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/96pn5d84f0chcg248pgrp5hel9.jpg',
    square: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/96pn5d84f0chcg248pgrp5hel9.__01_SX984_CR0,0,984,984__.jpg',
    time: 153.7728750705719
  },
  {
    original: 'https://images-na.ssl-images-amazon.com/images/I/415Xnu0pwBL.__01_SX120_CR0,0,120,120__.jpg',
    full: 'https://images-na.ssl-images-amazon.com/images/I/415Xnu0pwBL.jpg',
    square: 'https://images-na.ssl-images-amazon.com/images/I/415Xnu0pwBL.__01_SX371_CR0,0,371,371__.jpg',
    time: 110.14437508583069
  },
  {
    original: 'https://images-na.ssl-images-amazon.com/images/I/61w3pqVMCZL.__01_SX120_CR0,0,120,120__.jpg',
    full: 'https://images-na.ssl-images-amazon.com/images/I/61w3pqVMCZL.jpg',
    square: 'https://images-na.ssl-images-amazon.com/images/I/61w3pqVMCZL.__01_SX547_CR0,0,547,547__.jpg',
    time: 139.97262501716614
  },
  {
    original: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/u65m852f5j96v6ud70hpfb3hu7.__01_SX120_CR0,0,120,120__.jpg',
    full: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/u65m852f5j96v6ud70hpfb3hu7.jpg',
    square: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/u65m852f5j96v6ud70hpfb3hu7.__01_SX2796_CR0,0,2796,2796__.jpg',
    time: 292.4728749990463
  },
  {
    original: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/rn4hchq6ouadmsu04f1qjs0j3k.__01_SX120_CR0,0,120,120__.jpg',
    full: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/rn4hchq6ouadmsu04f1qjs0j3k.jpg',
    square: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/rn4hchq6ouadmsu04f1qjs0j3k.__01_SX2078_CR0,0,2078,2078__.jpg',
    time: 268.51145792007446
  },
  {
    original: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/fkeglaqq0pic05a0v6ieqt4iv5.__01_SX120_CR0,0,120,120__.jpg',
    full: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/fkeglaqq0pic05a0v6ieqt4iv5.jpg',
    square: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/fkeglaqq0pic05a0v6ieqt4iv5.__01_SX768_CR0,0,768,768__.jpg',
    time: 127.38229095935822
  },
  {
    original: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/11q81821d0ovjemedg11f7knb8.__01_SX120_CR0,0,120,120__.jpg',
    full: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/11q81821d0ovjemedg11f7knb8.jpg',
    square: 'https://images-na.ssl-images-amazon.com/images/S/amzn-author-media-prod/11q81821d0ovjemedg11f7knb8.__01_SX236_CR0,0,236,236__.jpg',
    time: 109.1701248884201
  },
  {
    original: 'https://images-na.ssl-images-amazon.com/images/I/B1PNLDB9k9S.__01_SX120_CR0,0,120,120__.jpg',
    full: 'https://images-na.ssl-images-amazon.com/images/I/B1PNLDB9k9S.jpg',
    square: 'https://images-na.ssl-images-amazon.com/images/I/B1PNLDB9k9S.__01_SX4912_CR0,0,4912,4912__.jpg',
    time: 263.32591700553894
  },
  {
    original: 'https://images-na.ssl-images-amazon.com/images/I/51rgIda7HDL.__01_SX120_CR0,0,120,120__.jpg',
    full: 'https://images-na.ssl-images-amazon.com/images/I/51rgIda7HDL.jpg',
    square: 'https://images-na.ssl-images-amazon.com/images/I/51rgIda7HDL.__01_SX315_CR0,0,315,315__.jpg',
    time: 146.03679203987122
  }
]
average time: 133.7502777212196
for 36 results

So an average of 134ms for getting the square image URL. This would always be in addition to the original processing time as you need the original image to be able to determine the square image URL in the first place.

I did however notice that there is a weird side case for this. In some cases, Audible appears to not do any cropping for their author images, and has a white border even for the thumbnail image:

image

I am not sure why this is the case but if I had to guess, it seems like it happens when they don't use any facial recognition to find the center of the image. Possibly because they couldn't reliably find a result for that image. This leads to a problem where the large version of that image also has white borders:

B1PNLDB9k9S __01_SX4912_CR0,0,4912,4912__-min

while the original image without resizing has no borders:

B1PNLDB9k9S

At first I thought this was happening specifically in cases where the image was too small but this specific example is massive so I doubt that's it. After checking these 36 example images, I found that 7 of them fell into this category, or just under 20%. This may be enough of an issue to consider this not to be a reliable method, however for many of the examples that did work, the centering is a massive improvement over the automatic centering that Plex does. I'm curious about your thoughts!

csandman commented 1 year ago

One alternative to adding this directly into Audnexus could be to add it into the Plex agent instead. I actually tried doing that myself before, but Python isn't my best language, and I've been struggling with what's available to use in Plex agents. This was where I started with the ArtistUpdateTool:

from PIL import ImageFile

# ...

class ArtistUpdateTool:

    # ...

    def get_square_image(self, img_url):
        """
            Get square image from Audible
        """
        try:
            file = urllib.urlopen(img_url)

            p = ImageFile.Parser()

            while 1:
                s = file.read(1024)
                if not s:
                    break
                p.feed(s)

            file.close()

            img_dims = p.close().size

            smaller_img_dim = str(min(img_dims))

            square_img_url = img_url.replace(
                '.jpg',
                '.__01_SX'+smaller_img_dim+'_CR0,0'+smaller_img_dim+','+smaller_img_dim+'__.jpg'
            )

            return square_img_url
        except Exception as err:
            log.separator(
                msg=(
                    'Error getting square image for ' + self.metadata.title + '\n' + err
                ),
                log_level="error"
            )
            return img_url

    # ...

    def parse_api_response(self, response):
        """
            Parses keys from API into helper variables if they exist.
        """
        if 'description' in response:
            self.description = response['description']
        if 'genres' in response:
            self.genres = response['genres']
        if 'name' in response:
            self.name = response['name']
        if 'image' in response:
            self.thumb = self.get_square_image(response['image'])

And I got there by following this: https://stackoverflow.com/questions/7460218/get-image-size-without-downloading-it-in-python

I'm not sure exactly what went wrong, but I didn't go super far with it. I'm not even sure if PIL is an available package in Plex agents. This might be a better alternative using native libs but you'd still have to handle the image downloading: https://stackoverflow.com/questions/8032642/how-to-obtain-image-size-using-standard-python-class-without-using-external-lib

Anyway, food for thought!

djdembeck commented 1 year ago

Putting this into the agents is the best move since that is where you wanted the centered images to begin with. I know urllib is available, but I'm not sure ImageFile is, as I've not heard of that before. It's possible to bundle the libraries we want to use into the plugin itself, though I've never tried it.

Your Python is quite good, just lacking clearly labeled variables (there's no need to shorten 'dim' or 'img').

djdembeck commented 1 year ago

I might also square images in AudiobookDB because it would be quite useful there. I had a personal loss around when I was nearly 'done' writing the MVP in March, and I could never get it squared up after that. Trying for round 2 this year :stuck_out_tongue:

csandman commented 1 year ago

Your Python is quite good, just lacking clearly labeled variables (there's no need to shorten 'dim' or 'img').

Haha fair enough, this wasn't really meant to be final code, mostly just what I copied and pasted from stack overflow and tweaked slightly. I more meant that I'm not the best at debugging python code, especially not through an agent running on my Plex server.

I tried doing this again using only the jpeg portion of this stackoverflow answer, because that's the only file type Audible returns, but I was still running into issues and wasn't sure how to debug it properly. This one uses only urllib so it would be a simpler solution if we could get it to work right:

    def get_square_image(self, image_url):
        """
            Get square image from Audible
        """
        try:
            file = urllib.urlopen(image_url)

            head = file.read(24)
            if len(head) != 24:
                return

            file.seek(0)  # Read 0xff next
            size = 2
            ftype = 0
            while not 0xc0 <= ftype <= 0xcf:
                file.seek(size, 1)
                byte = file.read(1)
                while ord(byte) == 0xff:
                    byte = file.read(1)
                ftype = ord(byte)
                size = struct.unpack('>H', file.read(2))[0] - 2
            # We are at a SOFn block
            file.seek(1, 1)  # Skip `precision' byte.
            height, width = struct.unpack('>HH', file.read(4))

            smaller_image_dimension = str(min(height, width))

            square_image_url = image_url.replace(
                '.jpg',
                '.__01_SX'+smaller_image_dimension+'_CR0,0' +
                smaller_image_dimension+','+smaller_image_dimension+'__.jpg'
            )

            return square_image_url
        except Exception as err:
            log.separator(
                msg=(
                    'Error getting square image for ' + self.metadata.title + '\n' + err
                ),
                log_level="error"
            )
        return image_url
djdembeck commented 1 year ago

No worries! My flow is to:

That being said, there were a couple of issues I patched. Namely exception catching section, since media.title is where the title seems to be for artists, and err is not a string, so it can't concatenate with the rest of the string. I added it as a separate ERROR level log.

import struct
import urllib

...

class ArtistUpdateTool(UpdateTool):
    def get_square_image(self, image_url):
        """
            Get square image from Audible
        """
        try:
            file = urllib.urlopen(image_url)

            head = file.read(24)
            if len(head) != 24:
                return

            file.seek(0)  # Read 0xff next
            size = 2
            ftype = 0
            while not 0xc0 <= ftype <= 0xcf:
                file.seek(size, 1)
                byte = file.read(1)
                while ord(byte) == 0xff:
                    byte = file.read(1)
                ftype = ord(byte)
                size = struct.unpack('>H', file.read(2))[0] - 2
            # We are at a SOFn block
            file.seek(1, 1)  # Skip `precision' byte.
            height, width = struct.unpack('>HH', file.read(4))

            smaller_image_dimension = str(min(height, width))

            square_image_url = image_url.replace(
                '.jpg',
                '.__01_SX'+smaller_image_dimension+'_CR0,0' +
                smaller_image_dimension+','+smaller_image_dimension+'__.jpg'
            )

            return square_image_url
        except Exception as err:
            log.separator(
                msg=(
                    'Error getting square image for ' + self.media.title
                ),
                log_level="error"
            )
            log.error(err)
        return image_url

    def parse_api_response(self, response):
        """
            Parses keys from API into helper variables if they exist.
        """
        # Set empty variables
        self.date = None
        self.genres = None
        self.thumb = ''

        if 'description' in response:
            self.description = response['description']
        if 'genres' in response:
            self.genres = response['genres']
        if 'name' in response:
            self.name = response['name']
        if 'image' in response:
            squared_image = self.get_square_image(response['image'])
            log.debug('Here is a square we found: ' + squared_image)
            self.thumb = response['image']
2023-01-23 14:14:39,730 (1455a56cdb38) :  INFO (sandbox:19) - -----------------------------------Error getting square image for Tom Robbins-----------------------------------
2023-01-23 14:14:39,731 (1455a56cdb38) :  ERROR (sandbox:19) - addinfourl instance has no attribute 'seek'

Now the error is seek is not usable with urlopen

csandman commented 1 year ago

Now the error is seek is not usable with urlopen

Ah yes, I wasn't sure if that would work for a requested file. So I'd assume to make this work, the requested file would have to be loaded in as a file object. I'm not sure what the best way to do that would be, I guess it could be saved to disk as a temporary file and then loaded in as a file object.

Alternatively, it could be possible to do this with file streaming but again, not too familiar with how one might do that in Python haha. I guess I'll look into it using a temp file.

Btw, do you know what version of python the Plex agents run on? Is it some version of Python 2?

djdembeck commented 1 year ago

They run on Python 2.7. Certain builtins can be imported easily, others have to be added as a library into the project and imported that way.

csandman commented 1 year ago

They run on Python 2.7. Certain builtins can be imported easily, others have to be added as a library into the project and imported that way.

So I figured it would be easy enough to just use the urllib.urlretrieve function, which is inherently meant to save a file to disk as a temp file. Then it should be possible to open a file as a File Object like in that stackoverflow example, and it would be deleted automatically afterwards.

Here is what I tried:

    def get_square_image(self, image_url):
        """
            Get square image from Audible
        """
        try:
            file_name, file_headers = urllib.urlretrieve(image_url)
            log.debug('Temp File Name: ' + file_name)

            with open(file_name, 'rb') as fhandle:
                head = fhandle.read(24)
                if len(head) != 24:
                    return image_url

                fhandle.seek(0)  # Read 0xff next
                size = 2
                ftype = 0
                while not 0xc0 <= ftype <= 0xcf:
                    fhandle.seek(size, 1)
                    byte = fhandle.read(1)
                    while ord(byte) == 0xff:
                        byte = fhandle.read(1)
                    ftype = ord(byte)
                    size = struct.unpack('>H', fhandle.read(2))[0] - 2
                # We are at a SOFn block
                fhandle.seek(1, 1)  # Skip `precision' byte.
                height, width = struct.unpack('>HH', fhandle.read(4))

                fhandle.close()

                smaller_image_dimension = str(min(height, width))

                square_image_url = image_url.replace(
                    '.jpg',
                    '.__01_SX'+smaller_image_dimension+'_CR0,0' +
                    smaller_image_dimension+','+smaller_image_dimension+'__.jpg'
                )

                return square_image_url
        except Exception as err:
            log.separator(
                msg=(
                    'Error getting square image for ' + self.media.title
                ),
                log_level="error"
            )
            log.error(err)
        return image_url

But now I'm getting the error global name 'open' is not defined. I can't find anything on the internet that would explain why this built-in function isn't defined. Is this part of what you meant by "certain builtins can be imported easily"? Because I don't know where I would import it from if so.

djdembeck commented 1 year ago

Probably not compiled for their plugin system. We could use their Data system instead. I've attached the documentation they have for it. Plex_data.pdf PlexData2.pdf

csandman commented 1 year ago

Ok, I figured out a way to do it! I ended up using the os.tmpfile function, and loaded the result of urllib.urlopen into it. That way, the seek function would be available and the file would still be removed automatically anyway.

Here's my final code:

import struct
import urllib
import os

# ...

class ArtistUpdateTool:

    # ...

    def get_square_image(self, image_url):
        """
            Get square image from Audible
        """
        try:
            image_file_dl = urllib.urlopen(image_url)
            image_file_dl_contents = image_file_dl.read()
            image_file_dl.close()

            image_file = os.tmpfile()
            image_file.write(image_file_dl_contents)

            image_file.seek(0)

            head = image_file.read(24)
            if len(head) != 24:
                image_file.close()
                return image_url

            image_file.seek(0)  # Read 0xff next
            size = 2
            ftype = 0
            while not 0xc0 <= ftype <= 0xcf:
                image_file.seek(size, 1)
                byte = image_file.read(1)
                while ord(byte) == 0xff:
                    byte = image_file.read(1)
                ftype = ord(byte)
                size = struct.unpack('>H', image_file.read(2))[0] - 2
            # We are at a SOFn block
            image_file.seek(1, 1)  # Skip `precision' byte.
            height, width = struct.unpack('>HH', image_file.read(4))

            image_file.close()

            smaller_image_dimension = str(min(height, width))

            square_image_url = image_url.replace(
                '.jpg',
                '.__01_SX'+smaller_image_dimension+'_CR0,0,' +
                smaller_image_dimension+','+smaller_image_dimension+'__.jpg'
            )

            return square_image_url
        except Exception as err:
            log.separator(
                msg=(
                    'Error getting square image for ' + self.media.title
                ),
                log_level="error"
            )
            log.error(err)
        return image_url

    def parse_api_response(self, response):
        """
            Parses keys from API into helper variables if they exist.
        """
        # Set empty variables
        self.date = None
        self.genres = None
        self.thumb = ''

        if 'description' in response:
            self.description = response['description']
        if 'genres' in response:
            self.genres = response['genres']
        if 'name' in response:
            self.name = response['name']
        if 'image' in response:
            squared_image = self.get_square_image(response['image'])
            log.debug('Here is a square we found: ' + squared_image)
            self.thumb = squared_image

I tried running a full metadata refresh on a test library, and it runs pretty quickly!


Now, to my point before about what they look like. Many of the resulting images look much better in my opinion, however for the images that appear to not have facial recognition, it adds a white border like I mentioned before.

Here is a comparison, so you can see for yourself:

With Square Function

image

Without Square Function

image

So here are my thoughts on the practicality of using this function;

Speed wise, I think there is no problem using this method, it seems to run great for me, but you'd have to try it yourself to see if you agree.

Visual wise, I think it makes a great improvement to many of the images. For some, it has relatively no impact as their faces are mostly centered anyway, but there are many cases where it does help. I did the results of my relatively small sample size:

Out of my 37 authors that the agent was able to find matches/images for:

So overall, the failure rate of this technique is relatively low, but it really depends on how much you dislike the white borders.

I think there are two possible ways implementing this could be approached:

  1. If it is possible, adding both the square and full sized images to each author would probably be ideal. I'm not sure if that is possible, or you know how that could be accomplished, but I notice for the Plex Music agent, multiple artist images are pulled for each. I think regardless of approach, this would be a nice to have option, so people could switch between them in cases where centering helps or hurts the author's image. I noticed that when I was working on the code to get the square images, the old images were still left in the authors' poster selection page so it's obviously possible to have both, I'm just not sure if they're both possible to add with the initial metadata grab.
  2. Adding a new option for choosing which image people prefer could be a good alternative. Depending on how much people dislike the borders, they could choose to use the default images or the centered ones. Adding both of them to the author would not hurt this option either, as they could always switch them manually if they chose to, and this would still allow them to pick which they'd like to default to.

Sorry for the long-winded comment, but seeing as this solution doesn't have a 100% success rate, I think it's worth discussing the implementation. If you'd like me to submit a PR to that repo with the new custom agent option, I'd be happy to do so! And I can also look into how to make the agent add both images to the artists if that's something you'd be interested in.

djdembeck commented 1 year ago

This is excellent!

An option for squared photos would be the best approach, and on a complexity scale, relatively easy to implement. It's also easy to add both images.

I wish there were a way to determine when a 'white border' picture was used because those don't look great. However, the payoff of centered photos is higher than the inconvenience of a low percentage of them.

csandman commented 1 year ago

An option for squared photos would be the best approach, and on a complexity scale, relatively easy to implement. It's also easy to add both images.

I agree that this would be an ideal solution, especially if you know how to add both! Would you like me to submit the start of a PR with a new option implemented? You're welcome to take this code and do it yourself, but I also have no issue doing it! If you already know how to add both images to the profile, then I'd be cool letting you handle that in post, but I could easily implement the option part first if you'd like!

I wish there were a way to determine when a 'white border' picture was used because those don't look great. However, the payoff of centered photos is higher than the inconvenience of a low percentage of them.

I do agree that it's worth it personally. But you're right, it would be great if there was a way to identify them. I actually thought about this for a bit, but I couldn't come up with anything that didn't involve some form of image processing. The strange thing is, when it does return the bordered images, they also appear that way in Audible, even though they're tiny thumbnails. That would definitely imply to me that they don't know where to center the images. Overall, the whole author image thing is relatively new to Audible anyway, so I'm sure they're still working out the kinks.

The one approach I can think of would be to use an image processing package like PIL to check every pixel along the vertical borders and horizontal border to see if they're all white. If either set of borders is completely white, it's probably safe to assume the centering didn't work. However, this approach would probably increase processing time/power by a decent amount. Also, you'd need to bundle PIL (or some alternate image processing lib) with the agent in order to make that work in the first place. I might play around with it to see if it's even possible and what the impact would be, but I can't really see it being worth it ultimately. I'll also keep my eyes out for an alternative approach to the problem.

djdembeck commented 1 year ago

Yes, feel free to open a PR (target develop), and I will be able to add on top of it

csandman commented 1 year ago

Quick question about your develop branch, I tried making a fork directly off of your most recent version of that branch, and it appears that the author metadata isn't coming back at all. I changed the identifier and agent names, so Plex would recognize it as a new agent and not pull cached data from other instances of it, and nothing is showing up. Is it supposed to be broken right now? Should I fork the main branch and then submit a PR to the develop branch?

djdembeck commented 1 year ago

As we speak, I am working on the latest develop and not seeing issues. What are your logs saying?

djdembeck commented 1 year ago

I see the problem with the server

EDIT: https://github.com/laxamentumtech/audnexus/issues/485#issuecomment-1402786059

csandman commented 1 year ago

This is what I'm getting from a refresh on an author with one book:

2023-01-24 17:39:35,990 (147c176abb38) :  DEBUG (runtime:717) - Handling request GET /:/plugins/com.plexapp.agents.audnexus-develop/messaging/function/X0FnZW50S2l0OlNlYXJjaA__/Y2VyZWFsMQoxCmxpc3QKMApyMAo_/Y2VyZWFsMQoyCmRpY3QKZGljdAo2CnMyCmVuczQKbGFuZ2IwczYKbWFudWFsYjFzNwpwcmltYXJ5aTAKczcKdmVyc2lvbnIxCnM2Cmt3YXJnc3M2CkFydGlzdHMxMAptZWRpYV90eXBlOQpzMTUKQSBQcm9taXNlZCBMYW5kczUKYWxidW1zMTYKZmQ5Mzk2YmM1OWQwMDk2MHMxNwpvcGVuU3VidGl0bGVzSGFzaHMxNQpBIFByb21pc2VkIExhbmRzNApuYW1lbnM1CnRpdGxlczEyCkJhcmFjayBPYmFtYXM2CmFydGlzdHMxMDAKJTJGZGF0YSUyRmJvb2tzJTJGQXVkaW9ib29rcyUyRk00QiUyRkJhcmFjayUyME9iYW1hJTJGQmFyYWNrJTIwT2JhbWElMjAtJTIwQSUyMFByb21pc2VkJTIwTGFuZCUyRW00YnM4CmZpbGVuYW1lczQwCmQ2N2E0OTBhYmFhNDFlNTcwYmZhMTNiNGFjYzQzMzg5MDZhOWQyNTNzOApwbGV4SGFzaHM5CjEwNTAxNjU2NnM4CmR1cmF0aW9uczYKMTMzNDgzczIKaWRyMAo_
2023-01-24 17:39:35,994 (147c176abb38) :  DEBUG (runtime:49) - Received packed state data (80 bytes)
2023-01-24 17:39:35,995 (147c176abb38) :  DEBUG (runtime:814) - Found route matching /:/plugins/com.plexapp.agents.audnexus-develop/messaging/function/X0FnZW50S2l0OlNlYXJjaA__/Y2VyZWFsMQoxCmxpc3QKMApyMAo_/Y2VyZWFsMQoyCmRpY3QKZGljdAo2CnMyCmVuczQKbGFuZ2IwczYKbWFudWFsYjFzNwpwcmltYXJ5aTAKczcKdmVyc2lvbnIxCnM2Cmt3YXJnc3M2CkFydGlzdHMxMAptZWRpYV90eXBlOQpzMTUKQSBQcm9taXNlZCBMYW5kczUKYWxidW1zMTYKZmQ5Mzk2YmM1OWQwMDk2MHMxNwpvcGVuU3VidGl0bGVzSGFzaHMxNQpBIFByb21pc2VkIExhbmRzNApuYW1lbnM1CnRpdGxlczEyCkJhcmFjayBPYmFtYXM2CmFydGlzdHMxMDAKJTJGZGF0YSUyRmJvb2tzJTJGQXVkaW9ib29rcyUyRk00QiUyRkJhcmFjayUyME9iYW1hJTJGQmFyYWNrJTIwT2JhbWElMjAtJTIwQSUyMFByb21pc2VkJTIwTGFuZCUyRW00YnM4CmZpbGVuYW1lczQwCmQ2N2E0OTBhYmFhNDFlNTcwYmZhMTNiNGFjYzQzMzg5MDZhOWQyNTNzOApwbGV4SGFzaHM5CjEwNTAxNjU2NnM4CmR1cmF0aW9uczYKMTMzNDgzczIKaWRyMAo_
2023-01-24 17:39:35,995 (147c176abb38) :  INFO (agentkit:961) - Searching for matches for {'album': 'A Promised Land', 'openSubtitlesHash': 'fd9396bc59d00960', 'name': 'A Promised Land', 'artist': 'Barack Obama', 'title': None, 'filename': '%2Fdata%2Fbooks%2FAudiobooks%2FM4B%2FBarack%20Obama%2FBarack%20Obama%20-%20A%20Promised%20Land%2Em4b', 'plexHash': 'd67a490abaa41e570bfa13b4acc4338906a9d253', 'duration': '105016566', 'id': '133483'}
2023-01-24 17:39:35,996 (147c176abb38) :  DEBUG (networking:143) - Requesting 'http://127.0.0.1:32400/library/metadata/133483/tree'
2023-01-24 17:39:36,015 (147c176abb38) :  DEBUG (networking:143) - Requesting 'https://api.audnex.us/authors?region=us&name=Barack%20Obama'
2023-01-24 17:39:36,296 (147c176abb38) :  ERROR (networking:196) - Error opening URL 'https://api.audnex.us/authors?region=us&name=Barack%20Obama'
2023-01-24 17:39:36,297 (147c176abb38) :  CRITICAL (agentkit:1018) - Exception in the search function of agent named 'Audnexus Agent (develop)', called with keyword arguments {'album': 'A Promised Land', 'openSubtitlesHash': 'fd9396bc59d00960', 'name': 'A Promised Land', 'artist': 'Barack Obama', 'title': None, 'filename': '%2Fdata%2Fbooks%2FAudiobooks%2FM4B%2FBarack%20Obama%2FBarack%20Obama%20-%20A%20Promised%20Land%2Em4b', 'plexHash': 'd67a490abaa41e570bfa13b4acc4338906a9d253', 'duration': '105016566', 'id': '133483'} (most recent call last):
  File "/usr/lib/plexmediaserver/Resources/Plug-ins-915986d62/Framework.bundle/Contents/Resources/Versions/2/Python/Framework/api/agentkit.py", line 1011, in _search
    agent.search(*f_args, **f_kwargs)
  File "/config/Library/Application Support/Plex Media Server/Plug-ins/Audnexus-develop.bundle/Contents/Code/__init__.py", line 88, in search
    result = self.call_search_api(search_helper)
  File "/config/Library/Application Support/Plex Media Server/Plug-ins/Audnexus-develop.bundle/Contents/Code/__init__.py", line 180, in call_search_api
    request = str(make_request(search_url))
  File "/usr/lib/plexmediaserver/Resources/Plug-ins-915986d62/Framework.bundle/Contents/Resources/Versions/2/Python/Framework/components/networking.py", line 220, in __str__
    self.load()
  File "/usr/lib/plexmediaserver/Resources/Plug-ins-915986d62/Framework.bundle/Contents/Resources/Versions/2/Python/Framework/components/networking.py", line 158, in load
    f = self._opener.open(req, timeout=self._timeout)
  File "/usr/lib/plexmediaserver/Resources/Python/python27.zip/urllib2.py", line 435, in open
    response = meth(req, response)
  File "/usr/lib/plexmediaserver/Resources/Python/python27.zip/urllib2.py", line 548, in http_response
    'http', request, response, code, msg, hdrs)
  File "/usr/lib/plexmediaserver/Resources/Python/python27.zip/urllib2.py", line 473, in error
    return self._call_chain(*args)
  File "/usr/lib/plexmediaserver/Resources/Python/python27.zip/urllib2.py", line 407, in _call_chain
    result = func(*args)
  File "/usr/lib/plexmediaserver/Resources/Python/python27.zip/urllib2.py", line 556, in http_error_default
    raise HTTPError(req.get_full_url(), code, msg, hdrs, fp)
HTTPError: HTTP Error 500: Internal Server Error

2023-01-24 17:39:36,298 (147c176abb38) :  DEBUG (runtime:88) - Sending packed state data (116 bytes)
2023-01-24 17:39:36,298 (147c176abb38) :  DEBUG (runtime:924) - Response: [200] str, 16 bytes
2023-01-24 17:39:36,419 (147c176abb38) :  DEBUG (runtime:717) - Handling request GET /:/plugins/com.plexapp.agents.audnexus-develop/messaging/function/X0FnZW50S2l0OlVwZGF0ZU1ldGFkYXRh/Y2VyZWFsMQoxCmxpc3QKMApyMAo_/Y2VyZWFsMQoxCmRpY3QKMTAKczIKZW5zNApsYW5nYjFzNQpmb3JjZWIwczgKcGVyaW9kaWNzNgoxMzM0ODRzNApkYmlkaTAKczcKdmVyc2lvbnMxNApsb2NhbDovLzEzMzQ4M3MxMApwYXJlbnRHVUlEczYKMTMzNDgzczgKcGFyZW50SURzNQpBbGJ1bXMxMAptZWRpYV90eXBlczU5CmNvbS5wbGV4YXBwLmFnZW50cy5hdWRuZXh1cy1kZXZlbG9wOi8vMDUyNTYzMzcyM191cz9sYW5nPWVuczQKZ3VpZHMxMwowNTI1NjMzNzIzX3VzczIKaWRyMAo_
2023-01-24 17:39:36,419 (147c176abb38) :  DEBUG (runtime:49) - Received packed state data (80 bytes)
2023-01-24 17:39:36,421 (147c176abb38) :  DEBUG (runtime:814) - Found route matching /:/plugins/com.plexapp.agents.audnexus-develop/messaging/function/X0FnZW50S2l0OlVwZGF0ZU1ldGFkYXRh/Y2VyZWFsMQoxCmxpc3QKMApyMAo_/Y2VyZWFsMQoxCmRpY3QKMTAKczIKZW5zNApsYW5nYjFzNQpmb3JjZWIwczgKcGVyaW9kaWNzNgoxMzM0ODRzNApkYmlkaTAKczcKdmVyc2lvbnMxNApsb2NhbDovLzEzMzQ4M3MxMApwYXJlbnRHVUlEczYKMTMzNDgzczgKcGFyZW50SURzNQpBbGJ1bXMxMAptZWRpYV90eXBlczU5CmNvbS5wbGV4YXBwLmFnZW50cy5hdWRuZXh1cy1kZXZlbG9wOi8vMDUyNTYzMzcyM191cz9sYW5nPWVuczQKZ3VpZHMxMwowNTI1NjMzNzIzX3VzczIKaWRyMAo_
2023-01-24 17:39:36,421 (147c176abb38) :  DEBUG (model:32) - Loading model with GUID com.plexapp.agents.audnexus-develop://0525633723_us?lang=en
2023-01-24 17:39:36,422 (147c176abb38) :  DEBUG (model:234) - Deserializing from /config/Library/Application Support/Plex Media Server/Metadata/Albums/2/bb7e71469cba753bdf6fb361e4577b79c285708.bundle/Contents/com.plexapp.agents.audnexus-develop/Info.xml
2023-01-24 17:39:36,424 (147c176abb38) :  DEBUG (networking:143) - Requesting 'http://127.0.0.1:32400/library/metadata/133484/tree'
2023-01-24 17:39:36,442 (147c176abb38) :  DEBUG (networking:143) - Requesting 'https://api.audnex.us/books/0525633723?region=us'
2023-01-24 17:39:36,592 (147c176abb38) :  ERROR (networking:196) - Error opening URL 'https://api.audnex.us/books/0525633723?region=us'
2023-01-24 17:39:36,593 (147c176abb38) :  CRITICAL (agentkit:1095) - Exception in the update function of agent named 'Audnexus Agent (develop)', called with guid 'com.plexapp.agents.audnexus-develop://0525633723_us?lang=en' (most recent call last):
  File "/usr/lib/plexmediaserver/Resources/Plug-ins-915986d62/Framework.bundle/Contents/Resources/Versions/2/Python/Framework/api/agentkit.py", line 1093, in _update
    agent.update(obj, media, lang, **kwargs)
  File "/config/Library/Application Support/Plex Media Server/Plug-ins/Audnexus-develop.bundle/Contents/Code/__init__.py", line 382, in update
    self.call_item_api(update_helper)
  File "/config/Library/Application Support/Plex Media Server/Plug-ins/Audnexus-develop.bundle/Contents/Code/__init__.py", line 460, in call_item_api
    request = str(make_request(update_url))
  File "/usr/lib/plexmediaserver/Resources/Plug-ins-915986d62/Framework.bundle/Contents/Resources/Versions/2/Python/Framework/components/networking.py", line 220, in __str__
    self.load()
  File "/usr/lib/plexmediaserver/Resources/Plug-ins-915986d62/Framework.bundle/Contents/Resources/Versions/2/Python/Framework/components/networking.py", line 158, in load
    f = self._opener.open(req, timeout=self._timeout)
  File "/usr/lib/plexmediaserver/Resources/Python/python27.zip/urllib2.py", line 435, in open
    response = meth(req, response)
  File "/usr/lib/plexmediaserver/Resources/Python/python27.zip/urllib2.py", line 548, in http_response
    'http', request, response, code, msg, hdrs)
  File "/usr/lib/plexmediaserver/Resources/Python/python27.zip/urllib2.py", line 473, in error
    return self._call_chain(*args)
  File "/usr/lib/plexmediaserver/Resources/Python/python27.zip/urllib2.py", line 407, in _call_chain
    result = func(*args)
  File "/usr/lib/plexmediaserver/Resources/Python/python27.zip/urllib2.py", line 556, in http_error_default
    raise HTTPError(req.get_full_url(), code, msg, hdrs, fp)
HTTPError: HTTP Error 500: Internal Server Error

2023-01-24 17:39:36,594 (147c176abb38) :  DEBUG (model:229) - Serializing to /config/Library/Application Support/Plex Media Server/Metadata/Albums/2/bb7e71469cba753bdf6fb361e4577b79c285708.bundle/Contents/com.plexapp.agents.audnexus-develop/Info.xml
2023-01-24 17:39:36,597 (147c176abb38) :  DEBUG (runtime:88) - Sending packed state data (116 bytes)
2023-01-24 17:39:36,597 (147c176abb38) :  DEBUG (runtime:924) - Response: [200] str, 16 bytes
djdembeck commented 1 year ago

Please read the uptime status. I've shut down the servers until I have time to migrate them to a non-shit host

csandman commented 1 year ago

Which uptime status are you referring to?

EDIT: nevermind I found it

csandman commented 1 year ago

Oh wow, I hate to admit this, but I was completely wrong about what's happening with the white space. Not only that but I was wrong about how the images were centered 🤦

I was searching for a pattern in the images to see if I could find a reason why some of them didn't work properly, and finally I realized that they were all landscape images. Which then made me look at the URL pattern again.

.__01_SX120_CR0,0,120,120__.

I realized the SX120 part of this is just referring to the X dimension of the image, and if you replace it with SY it will choose the vertical axis as the shorter one. I tried converting one of the landscape images to that and ended up with this.

https://images-na.ssl-images-amazon.com/images/I/21dJC-mHlYL.__01_SY200_CR0,0,200,200__.jpg

image

This then made me realize that the image was just getting centered on the left size... so I checked the portrait images... and I realized there is no intelligent centering on them whatsoever, they're just being cropped to the top of the image. I feel quite dumb for not realizing this sooner.

I don't think all of this work was for nothing, however. Through this, I realized that most of Audible's author images that are in portrait mode are much better off being centered at the top of the image. Enough so that I thought they were using object recognition to determine the center point haha. So I still think there is some merit to this functionality, however I think it should only be applied to portrait images.

I think the best use case for this would be to check if the width is smaller, and if it is, center the author image at the top. If the height is smaller, I think it's better off just being horizontally centered. This could either be done with some simple math, or by just returning the original image URL you already have and let Plex handle the centering, which is probably the better solution.

I'm curious on your thoughts, or if you think there even needs to be an option for this anymore. I feel like the vertical centering could probably just be on by default, as that's the way Audible handles their Author images already. I honestly find it strange that they don't handle their landscape images in the same way, it's pretty odd looking when they have the white borders only on landscape images.


Also, sorry for misleading you here, I went way too deep down the wrong rabbit hole haha 😅

djdembeck commented 1 year ago

Closed since this was merged on the client side. Great work as always Chris!