fix(ui/import): read author, series and volume out of the download folder name
Build image / image (push) Successful in 1m38s
Build image / image (push) Successful in 1m38s
This commit is contained in:
1 parent
13e2a0693e
commit
c8d4d239ec
1 file changed
+106
-17
+106
-17
@@ -1010,8 +1010,8 @@ $("#import-deselect-all").addEventListener("click", () => {
|
||||
let namesTargets = [];
|
||||
|
||||
function nameToRequest(item) {
|
||||
const { number, rest } = splitEpisodeNumber(item.name);
|
||||
return { title: cleanFileName(rest) || item.name, volume: number };
|
||||
const { title, volume } = parseDownloadName(item.name);
|
||||
return { title, volume };
|
||||
}
|
||||
|
||||
$("#import-from-names").addEventListener("click", () => {
|
||||
@@ -1023,8 +1023,10 @@ $("#import-from-names").addEventListener("click", () => {
|
||||
const type = namesTargets[0].media_type;
|
||||
namesTargets = namesTargets.filter((it) => it.media_type === type);
|
||||
$("#names-intro").textContent = t("names.intro", { n: namesTargets.length, type });
|
||||
$("#names-series").value = lastSeries;
|
||||
$("#names-authors").value = "";
|
||||
// the folder names usually carry both, e.g. "Autor - Serie 03 - Titel"
|
||||
const first = parseDownloadName(namesTargets[0].name);
|
||||
$("#names-series").value = first.series || lastSeries;
|
||||
$("#names-authors").value = first.authors;
|
||||
$("#names-library").innerHTML = libOptions(type) || noLibraryOption();
|
||||
renderNamesPreview();
|
||||
$("#names-dialog").showModal();
|
||||
@@ -1176,6 +1178,7 @@ function splitItem(i) {
|
||||
// ---- quick request from a scanned file ----
|
||||
let quickItemIndex = null;
|
||||
let quickEpisode = null;
|
||||
let quickSeries = ""; // series read from the folder name, if the hit has none
|
||||
let quickPicked = null; // metadata hit the manual fields were filled from
|
||||
let quickAutoSearch = false; // the search fired on open, not by the user
|
||||
let lastSeries = "";
|
||||
@@ -1200,29 +1203,115 @@ function splitEpisodeNumber(name) {
|
||||
return { number: null, rest: name };
|
||||
}
|
||||
|
||||
function cleanFileName(name) {
|
||||
// release noise: Audible treats every word of a query as required, so "6CD",
|
||||
// "vorgelesen" or a year answers a perfectly good folder name with nothing
|
||||
const RELEASE_TAG_RE = /\b(?:mp3cd|mp3|m4b|flac|ogg|opus|epub|pdf|azw3|cbz|cbr|retail|unabridged|ungek(?:ue|ü)rzt|gek(?:ue|ü)rzt|vorgelesen|abook|audiobook|h(?:oe|ö)rbuch|\d+\s?cds?|\d+\s?mp|(?:19|20)\d{2}|kompl.*)\b/gi;
|
||||
// "Grant County 01", "Georgia-Serie 11", "017": a trailing number is the volume
|
||||
const SERIES_VOLUME_RE = /^(.*?)[\s._-]*(?:reihe|serie)?[\s._-]*(\d{1,3})$/i;
|
||||
// "Folge 124", "Band 3": a counter word in front of the number is not a series
|
||||
const COUNTER_WORD_RE = /^(?:folge|folgen|teil|band|nr\.?|nummer|episode|ep|vol|volume|track|cd|disc)$/i;
|
||||
// a language tag from the release name; uppercase only, so a Spanish "de" stays
|
||||
const LANG_TAG_RE = /\b(?:DE|EN|GER|ENG|MULTI)\b/g;
|
||||
// "Karin Slaughter", "J R R Tolkien" - every word capitalised, no digits, so a
|
||||
// series like "Die drei ???" is not mistaken for an author
|
||||
const PERSON_RE = /^\p{Lu}[\p{L}.'’-]*(?: \p{Lu}[\p{L}.'’-]*){1,3}$/u;
|
||||
|
||||
function stripTags(name) {
|
||||
return name
|
||||
.replace(/[._\-\[\]()]+/g, " ")
|
||||
.replace(/\b(mp3|m4b|flac|epub|pdf|cbz|cbr|retail|unabridged|kompl.*)\b/gi, " ")
|
||||
.replace(LANG_TAG_RE, " ")
|
||||
.replace(RELEASE_TAG_RE, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function splitSeriesVolume(part) {
|
||||
const m = SERIES_VOLUME_RE.exec(part.trim());
|
||||
if (!m) return null;
|
||||
const head = m[1].trim();
|
||||
return { series: COUNTER_WORD_RE.test(head) ? "" : head, volume: parseInt(m[2], 10) };
|
||||
}
|
||||
|
||||
function cleanFileName(name) {
|
||||
return stripTags(name.replace(/[._\-\[\]()]+/g, " "));
|
||||
}
|
||||
|
||||
// Download folders follow a few scene conventions: "Autor - Serie 03 - Titel",
|
||||
// "Serie - 017 - Titel", "Autor - Titel (Serie 7)", "Autor_-_Titel-6CD-2013".
|
||||
// The series number and the tags must stay out of the search query, but the
|
||||
// leading segment must stay in it - for "Die drei ???" it is what Audible
|
||||
// titles the books after.
|
||||
function parseDownloadName(name) {
|
||||
let series = "", volume = null, authors = "", lead = "";
|
||||
let s = name.replace(/_/g, " ").replace(/(\p{L})\.(?=\p{L})/gu, "$1 ");
|
||||
// "(Georgia 7)" names the series, "(ADAC Motorwelt Edition)" is noise
|
||||
s = s.replace(/[[(]([^)\]]*)[)\]]/g, (_, inner) => {
|
||||
const sv = splitSeriesVolume(stripTags(inner));
|
||||
if (sv) {
|
||||
series = sv.series || series;
|
||||
volume = sv.volume;
|
||||
}
|
||||
return " ";
|
||||
});
|
||||
const parts = stripTags(s)
|
||||
.split(/\s+[-–—]+\s+/)
|
||||
.map((p) => p.replace(/^[\s.\-–—]+|[\s.\-–—]+$/g, ""))
|
||||
.filter(Boolean);
|
||||
if (parts.length >= 3) {
|
||||
const sv = splitSeriesVolume(parts[1]);
|
||||
if (sv) {
|
||||
volume = sv.volume;
|
||||
// a named middle part leaves the first one as the author, a bare number
|
||||
// ("Serie - 017 - Titel") as the series
|
||||
if (sv.series) {
|
||||
series = sv.series;
|
||||
authors = parts[0];
|
||||
} else {
|
||||
series = parts[0];
|
||||
}
|
||||
lead = parts.splice(0, 2)[0];
|
||||
}
|
||||
} else if (parts.length === 2) {
|
||||
// "Die drei ??? 017 - Titel": series and number share the first part
|
||||
const sv = splitSeriesVolume(parts[0]);
|
||||
if (sv && sv.series) {
|
||||
series = sv.series;
|
||||
volume = sv.volume;
|
||||
lead = sv.series;
|
||||
parts.shift();
|
||||
}
|
||||
}
|
||||
if (!lead && parts.length > 1 && PERSON_RE.test(parts[0])) {
|
||||
authors = lead = parts.shift();
|
||||
}
|
||||
let rest = parts.join(" - ");
|
||||
if (volume == null) {
|
||||
const ep = splitEpisodeNumber(rest);
|
||||
if (ep.number != null) {
|
||||
volume = ep.number;
|
||||
rest = ep.rest;
|
||||
}
|
||||
}
|
||||
const title = cleanFileName(rest) || cleanFileName(name) || name;
|
||||
return { title, volume, series, authors, query: cleanFileName(`${lead} ${rest}`) || title };
|
||||
}
|
||||
|
||||
function openQuickDialog(i) {
|
||||
quickItemIndex = i;
|
||||
const item = scanItems[i];
|
||||
// the number wrecks the Audible search, so it becomes the volume instead
|
||||
const { number, rest } = splitEpisodeNumber(item.name);
|
||||
quickEpisode = number;
|
||||
const parsed = parseDownloadName(item.name);
|
||||
quickEpisode = parsed.volume;
|
||||
quickSeries = parsed.series;
|
||||
$("#quick-file").textContent = (item.rel_dir ? item.rel_dir + "/" : "") + item.name;
|
||||
$("#quick-episode").textContent = number != null ? t("quick.episodeNote", { n: number }) : "";
|
||||
$("#quick-q").value = cleanFileName(rest);
|
||||
$("#quick-episode").textContent =
|
||||
parsed.volume != null ? t("quick.episodeNote", { n: parsed.volume }) : "";
|
||||
$("#quick-q").value = parsed.query;
|
||||
quickPicked = null;
|
||||
$("#quick-req-filter").value = "";
|
||||
$("#quick-m-title").value = cleanFileName(rest);
|
||||
$("#quick-m-authors").value = "";
|
||||
$("#quick-m-volume").value = number ?? "";
|
||||
$("#quick-m-series").value = lastSeries;
|
||||
$("#quick-m-title").value = parsed.title;
|
||||
$("#quick-m-authors").value = parsed.authors;
|
||||
$("#quick-m-volume").value = parsed.volume ?? "";
|
||||
$("#quick-m-series").value = parsed.series || lastSeries;
|
||||
$("#quick-based-on").hidden = true;
|
||||
$("#quick-manual-summary").textContent = t("quick.manualSummary");
|
||||
$("#quick-manual").open = false;
|
||||
@@ -1370,7 +1459,7 @@ function pickQuickResult(r) {
|
||||
return requestAndConnect({
|
||||
title: r.title, authors: r.authors, narrator: r.narrator || "",
|
||||
external_id: r.external_id, volume: r.volume ?? quickEpisode,
|
||||
year: r.year, series: r.series, cover_url: r.cover_url,
|
||||
year: r.year, series: r.series || quickSeries, cover_url: r.cover_url,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1380,7 +1469,7 @@ function editBeforeRequest(r) {
|
||||
quickPicked = r;
|
||||
$("#quick-m-title").value = r.title || "";
|
||||
$("#quick-m-authors").value = r.authors || "";
|
||||
$("#quick-m-series").value = r.series || "";
|
||||
$("#quick-m-series").value = r.series || quickSeries;
|
||||
$("#quick-m-volume").value = r.volume ?? quickEpisode ?? "";
|
||||
$("#quick-based-on").textContent = t("quick.basedOn", { title: r.title });
|
||||
$("#quick-based-on").hidden = false;
|
||||
|
||||
Reference in new issue
Block a user