mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-12-15 08:19:37 +00:00
New model update details, author and series inputs with create new, compare & copy utils
This commit is contained in:
parent
f2be3bc95e
commit
5f4e5cd3d8
19 changed files with 707 additions and 70 deletions
142
server/utils/areEquivalent.js
Normal file
142
server/utils/areEquivalent.js
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
/**
|
||||
* https://gist.github.com/DLiblik/96801665f9b6c935f12c1071d37eae95
|
||||
Compares two items (values or references) for nested equivalency, meaning that
|
||||
at root and at each key or index they are equivalent as follows:
|
||||
- If a value type, values are either hard equal (===) or are both NaN
|
||||
(different than JS where NaN !== NaN)
|
||||
- If functions, they are the same function instance or have the same value
|
||||
when converted to string via `toString()`
|
||||
- If Date objects, both have the same getTime() or are both NaN (invalid)
|
||||
- If arrays, both are same length, and all contained values areEquivalent
|
||||
recursively - only contents by numeric key are checked
|
||||
- If other object types, enumerable keys are the same (the keys themselves)
|
||||
and values at every key areEquivalent recursively
|
||||
Author: Dathan Liblik
|
||||
License: Free to use anywhere by anyone, as-is, no guarantees of any kind.
|
||||
@param value1 First item to compare
|
||||
@param value2 Other item to compare
|
||||
@param stack Used internally to track circular refs - don't set it
|
||||
*/
|
||||
module.exports = function areEquivalent(value1, value2, stack = []) {
|
||||
// Numbers, strings, null, undefined, symbols, functions, booleans.
|
||||
// Also: objects (incl. arrays) that are actually the same instance
|
||||
if (value1 === value2) {
|
||||
// Fast and done
|
||||
return true;
|
||||
}
|
||||
|
||||
const type1 = typeof value1;
|
||||
|
||||
// Ensure types match
|
||||
if (type1 !== typeof value2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Special case for number: check for NaN on both sides
|
||||
// (only way they can still be equivalent but not equal)
|
||||
if (type1 === 'number') {
|
||||
// Failed initial equals test, but could still both be NaN
|
||||
return (isNaN(value1) && isNaN(value2));
|
||||
}
|
||||
|
||||
// Special case for function: check for toString() equivalence
|
||||
if (type1 === 'function') {
|
||||
// Failed initial equals test, but could still have equivalent
|
||||
// implementations - note, will match on functions that have same name
|
||||
// and are native code: `function abc() { [native code] }`
|
||||
return value1.toString() === value2.toString();
|
||||
}
|
||||
|
||||
// For these types, cannot still be equal at this point, so fast-fail
|
||||
if (type1 === 'bigint' || type1 === 'boolean' ||
|
||||
type1 === 'function' || type1 === 'string' ||
|
||||
type1 === 'symbol') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// For dates, cast to number and ensure equal or both NaN (note, if same
|
||||
// exact instance then we're not here - that was checked above)
|
||||
if (value1 instanceof Date) {
|
||||
if (!(value2 instanceof Date)) {
|
||||
return false;
|
||||
}
|
||||
// Convert to number to compare
|
||||
const asNum1 = +value1, asNum2 = +value2;
|
||||
// Check if both invalid (NaN) or are same value
|
||||
return asNum1 === asNum2 || (isNaN(asNum1) && isNaN(asNum2));
|
||||
}
|
||||
|
||||
// At this point, it's a reference type and could be circular, so
|
||||
// make sure we haven't been here before... note we only need to track value1
|
||||
// since value1 being un-circular means value2 will either be equal (and not
|
||||
// circular too) or unequal whether circular or not.
|
||||
if (stack.includes(value1)) {
|
||||
throw new Error(`areEquivalent value1 is circular`);
|
||||
}
|
||||
|
||||
// breadcrumb
|
||||
stack.push(value1);
|
||||
|
||||
// Handle arrays
|
||||
if (Array.isArray(value1)) {
|
||||
if (!Array.isArray(value2)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const length = value1.length;
|
||||
|
||||
if (length !== value2.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (!areEquivalent(value1[i], value2[i], stack)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Final case: object
|
||||
|
||||
// get both key lists and check length
|
||||
const keys1 = Object.keys(value1);
|
||||
const keys2 = Object.keys(value2);
|
||||
const numKeys = keys1.length;
|
||||
|
||||
if (keys2.length !== numKeys) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Empty object on both sides?
|
||||
if (numKeys === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// sort is a native call so it's very fast - much faster than comparing the
|
||||
// values at each key if it can be avoided, so do the sort and then
|
||||
// ensure every key matches at every index
|
||||
keys1.sort();
|
||||
keys2.sort();
|
||||
|
||||
// Ensure perfect match across all keys
|
||||
for (let i = 0; i < numKeys; i++) {
|
||||
if (keys1[i] !== keys2[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure perfect match across all values
|
||||
for (let i = 0; i < numKeys; i++) {
|
||||
if (!areEquivalent(value1[keys1[i]], value2[keys1[i]], stack)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// back up
|
||||
stack.pop();
|
||||
|
||||
// Walk the same, talk the same - matching ducks. Quack.
|
||||
// 🦆🦆
|
||||
return true;
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ const Path = require('path')
|
|||
const fs = require('fs')
|
||||
const Logger = require('../Logger')
|
||||
const { parseString } = require("xml2js")
|
||||
const areEquivalent = require('./areEquivalent')
|
||||
|
||||
const levenshteinDistance = (str1, str2, caseSensitive = false) => {
|
||||
if (!caseSensitive) {
|
||||
|
|
@ -99,4 +100,21 @@ module.exports.msToTimestamp = (ms, includeMs) => secondsToTimestamp(ms / 1000,
|
|||
module.exports.reqSupportsWebp = (req) => {
|
||||
if (!req || !req.headers || !req.headers.accept) return false
|
||||
return req.headers.accept.includes('image/webp') || req.headers.accept === '*/*'
|
||||
}
|
||||
|
||||
module.exports.areEquivalent = areEquivalent
|
||||
|
||||
module.exports.copyValue = (val) => {
|
||||
if (!val) return null
|
||||
if (!this.isObject(val)) return val
|
||||
|
||||
if (Array.isArray(val)) {
|
||||
return val.map(this.copyValue)
|
||||
} else {
|
||||
var final = {}
|
||||
for (const key in val) {
|
||||
final[key] = this.copyValue(val[key])
|
||||
}
|
||||
return final
|
||||
}
|
||||
}
|
||||
|
|
@ -97,12 +97,12 @@ module.exports = {
|
|||
var mediaMetadata = li.media.metadata
|
||||
if (mediaMetadata.authors.length) {
|
||||
mediaMetadata.authors.forEach((author) => {
|
||||
if (author && !data.authors.includes(author.name)) data.authors.push(author.name)
|
||||
if (author && !data.authors.find(au => au.id === author.id)) data.authors.push({ id: author.id, name: author.name })
|
||||
})
|
||||
}
|
||||
if (mediaMetadata.series.length) {
|
||||
mediaMetadata.series.forEach((series) => {
|
||||
if (series && !data.series.includes(series.name)) data.series.push(series.name)
|
||||
if (series && !data.series.find(se => se.id === series.id)) data.series.push({ id: series.id, name: series.name })
|
||||
})
|
||||
}
|
||||
if (mediaMetadata.genres.length) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue