stash
This commit is contained in:
3
stash/config/plugins/community/filenameParser/README.md
Normal file
3
stash/config/plugins/community/filenameParser/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Filename parser
|
||||
|
||||
https://discourse.stashapp.cc/t/filename-parser/1378
|
||||
427
stash/config/plugins/community/filenameParser/filenameParser.js
Normal file
427
stash/config/plugins/community/filenameParser/filenameParser.js
Normal file
@@ -0,0 +1,427 @@
|
||||
function ok() {
|
||||
return {
|
||||
output: "ok",
|
||||
};
|
||||
}
|
||||
|
||||
function main() {
|
||||
var hookContext = input.Args.hookContext;
|
||||
var type = hookContext.type;
|
||||
var ID = hookContext.ID;
|
||||
|
||||
if (!ID) {
|
||||
return ok();
|
||||
}
|
||||
|
||||
var filenameFetcher;
|
||||
var saver;
|
||||
if (type === "Scene.Create.Post") {
|
||||
filenameFetcher = getSceneFilename;
|
||||
saver = updateScene;
|
||||
} else if (type === "Gallery.Create.Post") {
|
||||
filenameFetcher = getGalleryFilename;
|
||||
saver = updateGallery;
|
||||
} else {
|
||||
return ok();
|
||||
}
|
||||
|
||||
var filename = filenameFetcher(ID);
|
||||
if (!filename) {
|
||||
return ok();
|
||||
}
|
||||
|
||||
filename = cleanFilename(filename);
|
||||
var parseResult = parseFilename(filename);
|
||||
|
||||
saver(ID, parseResult);
|
||||
|
||||
return ok();
|
||||
}
|
||||
|
||||
function getSceneFilename(sceneID) {
|
||||
var query =
|
||||
"\
|
||||
query findScene($id: ID) {\
|
||||
findScene(id: $id) {\
|
||||
files {\
|
||||
path\
|
||||
}\
|
||||
}\
|
||||
}";
|
||||
|
||||
var variables = {
|
||||
id: sceneID,
|
||||
};
|
||||
|
||||
var result = gql.Do(query, variables);
|
||||
var findScene = result.findScene;
|
||||
if (!findScene) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var path = findScene.files[0].path;
|
||||
return path.substring(path.lastIndexOf("/") + 1);
|
||||
}
|
||||
|
||||
function updateScene(sceneID, parseResult) {
|
||||
var query =
|
||||
"\
|
||||
mutation SceneUpdate($input: SceneUpdateInput!) {\
|
||||
sceneUpdate(input: $input) {\
|
||||
id\
|
||||
}\
|
||||
}";
|
||||
|
||||
var variables = {
|
||||
input: parseResult,
|
||||
};
|
||||
|
||||
variables.input.id = sceneID;
|
||||
|
||||
gql.Do(query, variables);
|
||||
}
|
||||
|
||||
function getGalleryFilename(galleryID) {
|
||||
var query =
|
||||
"\
|
||||
query findGallery($id: ID!) {\
|
||||
findGallery(id: $id) {\
|
||||
files {\
|
||||
path\
|
||||
}\
|
||||
}\
|
||||
}";
|
||||
|
||||
var variables = {
|
||||
id: galleryID,
|
||||
};
|
||||
|
||||
var result = gql.Do(query, variables);
|
||||
var findGallery = result.findGallery;
|
||||
if (!findGallery) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var path = findGallery.files[0].path;
|
||||
return path.substring(path.lastIndexOf("/") + 1);
|
||||
}
|
||||
|
||||
function updateGallery(galleryID, parseResult) {
|
||||
var query =
|
||||
"\
|
||||
mutation GalleryUpdate($input: GalleryUpdateInput!) {\
|
||||
galleryUpdate(input: $input) {\
|
||||
id\
|
||||
}\
|
||||
}";
|
||||
|
||||
var variables = {
|
||||
input: parseResult,
|
||||
};
|
||||
|
||||
variables.input.id = galleryID;
|
||||
|
||||
gql.Do(query, variables);
|
||||
}
|
||||
|
||||
function matchNames(parts, name, aliases) {
|
||||
var names = [name].concat(aliases);
|
||||
|
||||
var partRegexes = [];
|
||||
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
partRegexes[i] = new RegExp("^" + parts[i].toLowerCase() + "[. \\-_]*");
|
||||
}
|
||||
|
||||
var cleanRegex = /[. \-_]/g;
|
||||
var longestMatch = 0;
|
||||
for (var i = 0; i < names.length; i++) {
|
||||
var name = names[i].replace(cleanRegex, "").toLowerCase();
|
||||
for (var j = 0; j < partRegexes.length; j++) {
|
||||
if (!partRegexes[j].test(name)) {
|
||||
break;
|
||||
}
|
||||
|
||||
name = name.replace(partRegexes[j], "");
|
||||
|
||||
if (name.length === 0) {
|
||||
if (j + 1 > longestMatch) {
|
||||
longestMatch = j + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return longestMatch;
|
||||
}
|
||||
|
||||
function cleanFilename(name) {
|
||||
name = name
|
||||
// remove imageset-...[rarbg]
|
||||
.replace(/imageset-[\w\d]+\[rarbg]/i, "")
|
||||
// replace [...] with just ...
|
||||
.replace(/\[(.*?)]/g, "$1")
|
||||
// replace (...) with just ...
|
||||
.replace(/\((.*?)\)/g, "$1")
|
||||
// replace {...} with just ...
|
||||
.replace(/{(.*?)}/g, "$1");
|
||||
|
||||
var blockList = [
|
||||
"mp4",
|
||||
"mov",
|
||||
"mkv",
|
||||
"zip",
|
||||
"cbz",
|
||||
"cbr",
|
||||
"xxx",
|
||||
"4k",
|
||||
"4096x2160",
|
||||
"3840x2160",
|
||||
"2160p",
|
||||
"1080p",
|
||||
"1920x1080",
|
||||
"60fps",
|
||||
"30fps",
|
||||
"repack",
|
||||
"ktr",
|
||||
];
|
||||
var regExp = new RegExp(
|
||||
"(_|[^\\w\\d]|^)(" + blockList.join("|") + ")(_|[^\\w\\d]|$)",
|
||||
"i"
|
||||
);
|
||||
while (regExp.test(name)) {
|
||||
name = name.replace(regExp, "$1$3");
|
||||
}
|
||||
|
||||
// If name starts with <...>.com remove the .com (sometimes names include studio name as site/domain)
|
||||
name = name.replace(/^([\w\d-]+?)\.com/, "$1");
|
||||
|
||||
name = name
|
||||
// Remove everything except letters and digits at the start
|
||||
.replace(/^(_|[^\w\d])+/, "")
|
||||
// Remove everything except letters and digits at the end
|
||||
.replace(/(_|[^\w\d])+$/, "");
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
function matchStudio(parts, result) {
|
||||
var query =
|
||||
"\
|
||||
query findStudios($studio_filter: StudioFilterType, $filter: FindFilterType!) {\
|
||||
findStudios(studio_filter: $studio_filter, filter: $filter) {\
|
||||
studios {\
|
||||
id\
|
||||
name\
|
||||
aliases\
|
||||
}\
|
||||
}\
|
||||
}";
|
||||
|
||||
var searchTerm = parts[0].substring(0, 2);
|
||||
if (parts[0].substring(0, 1) === "a") {
|
||||
searchTerm = parts[0].substring(1, 3);
|
||||
}
|
||||
var variables = {
|
||||
filter: {
|
||||
per_page: -1,
|
||||
},
|
||||
studio_filter: {
|
||||
name: {
|
||||
modifier: "INCLUDES",
|
||||
value: searchTerm,
|
||||
},
|
||||
OR: {
|
||||
aliases: {
|
||||
modifier: "INCLUDES",
|
||||
value: searchTerm,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var queryResult = gql.Do(query, variables);
|
||||
var studios = queryResult.findStudios.studios;
|
||||
if (!studios.length && parts[0].substring(0, 1) === "a") {
|
||||
variables.studio_filter.name.value =
|
||||
variables.studio_filter.OR.aliases.value = parts[0].substring(1, 3);
|
||||
queryResult = gql.Do(query, variables);
|
||||
studios = queryResult.findStudios.studios;
|
||||
}
|
||||
|
||||
var matchingParts = 0;
|
||||
for (var i = 0; i < studios.length; i++) {
|
||||
var studio = studios[i];
|
||||
matchingParts = matchNames(parts, studio.name, studio.aliases);
|
||||
if (matchingParts === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.studio_id = studio.id;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return matchingParts;
|
||||
}
|
||||
|
||||
function matchDate(parts, result) {
|
||||
if (
|
||||
parts.length < 3 ||
|
||||
!/^(\d{2}|\d{4})$/.test(parts[0]) ||
|
||||
!/^\d{2}$/.test(parts[1]) ||
|
||||
!/^\d{2}$/.test(parts[2])
|
||||
) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
var year = parseInt(parts[0], 10);
|
||||
var month = parseInt(parts[1], 10);
|
||||
var day = parseInt(parts[2], 10);
|
||||
|
||||
if (year < 100) {
|
||||
year += 2000;
|
||||
}
|
||||
|
||||
if (
|
||||
year < 2000 ||
|
||||
year > 2100 ||
|
||||
month < 1 ||
|
||||
month > 12 ||
|
||||
day < 1 ||
|
||||
day > 31
|
||||
) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
result.date =
|
||||
year +
|
||||
"-" +
|
||||
(month < 10 ? "0" + month : month) +
|
||||
"-" +
|
||||
(day < 10 ? "0" + day : day);
|
||||
|
||||
return 3;
|
||||
}
|
||||
|
||||
function matchPerformers(parts, result) {
|
||||
var query =
|
||||
"\
|
||||
query findPerformers($performer_filter: PerformerFilterType, $filter: FindFilterType!) {\
|
||||
findPerformers(performer_filter: $performer_filter, filter: $filter) {\
|
||||
performers {\
|
||||
id\
|
||||
name\
|
||||
alias_list\
|
||||
}\
|
||||
}\
|
||||
}";
|
||||
var variables = {
|
||||
filter: {
|
||||
per_page: -1,
|
||||
},
|
||||
performer_filter: {
|
||||
name: {
|
||||
modifier: "INCLUDES",
|
||||
},
|
||||
OR: {
|
||||
aliases: {
|
||||
modifier: "INCLUDES",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var totalMatchingParts = 0;
|
||||
result.performer_ids = [];
|
||||
do {
|
||||
variables.performer_filter.name.value =
|
||||
variables.performer_filter.OR.aliases.value = parts[0].substring(0, 2);
|
||||
|
||||
var queryResult = gql.Do(query, variables);
|
||||
var performers = queryResult.findPerformers.performers;
|
||||
if (!performers.length) {
|
||||
parts.shift();
|
||||
continue;
|
||||
}
|
||||
|
||||
var maxMatchLength = 0;
|
||||
var matches = [];
|
||||
for (var i = 0; i < performers.length; i++) {
|
||||
var performer = performers[i];
|
||||
var aliases = performer.aliases
|
||||
? performer.aliases.split(/\s*[,;]+\s*/)
|
||||
: [];
|
||||
var matchingParts = matchNames(parts, performer.name, aliases);
|
||||
if (matchingParts === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (matchingParts > maxMatchLength) {
|
||||
maxMatchLength = matchingParts;
|
||||
matches = [performer.id];
|
||||
} else if (matchingParts === maxMatchLength) {
|
||||
matches.push(performer.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (maxMatchLength === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
result.performer_ids = result.performer_ids.concat(matches);
|
||||
|
||||
totalMatchingParts += maxMatchLength;
|
||||
|
||||
parts = parts.slice(maxMatchLength);
|
||||
while (
|
||||
parts.length > 0 &&
|
||||
(parts[0].toLowerCase() === "and" || parts[0] === "&")
|
||||
) {
|
||||
parts.shift();
|
||||
totalMatchingParts += 1;
|
||||
}
|
||||
} while (parts.length > 0);
|
||||
|
||||
return totalMatchingParts;
|
||||
}
|
||||
|
||||
function parseFilename(name) {
|
||||
var parts = name.split(/[. \-_,]+/);
|
||||
|
||||
var matchers = [matchStudio, matchDate, matchPerformers];
|
||||
|
||||
var result = {};
|
||||
var hasMatched = false;
|
||||
for (
|
||||
var matchTries = 0;
|
||||
matchTries < 3 && !hasMatched && parts.length;
|
||||
matchTries++
|
||||
) {
|
||||
for (var i = 0; i < matchers.length && parts.length > 0; i++) {
|
||||
var matchedParts = matchers[i](parts, result);
|
||||
|
||||
if (matchedParts > 0) {
|
||||
hasMatched = true;
|
||||
parts = parts.slice(matchedParts);
|
||||
}
|
||||
}
|
||||
|
||||
// If no matchers worked remove a part. Maybe the format is correct but studio isn't found? etc
|
||||
if (!hasMatched) {
|
||||
parts.shift();
|
||||
}
|
||||
}
|
||||
|
||||
if (hasMatched && parts.length > 0) {
|
||||
var title = parts.join(" ");
|
||||
// Look behind assertions are not supported, so can't use `replace(/(?<=.)([A-Z]/g, ' $1')` so instead have to do a loop. Otherwise for example 'FooABar' will become 'Foo ABar' instead of 'Foo A Bar'
|
||||
while (/[^\s][A-Z]/.test(title)) {
|
||||
title = title.replace(/([^\s])([A-Z])/g, "$1 $2");
|
||||
}
|
||||
result.title = title.trim();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,13 @@
|
||||
name: Filename parser
|
||||
description: Parses filename into studio, date, performers and title
|
||||
url:
|
||||
version: 0.1
|
||||
exec:
|
||||
- filenameParser.js
|
||||
interface: js
|
||||
hooks:
|
||||
- name: Prepopulates data based on filename
|
||||
description:
|
||||
triggeredBy:
|
||||
- Scene.Create.Post
|
||||
- Gallery.Create.Post
|
||||
12
stash/config/plugins/community/filenameParser/manifest
Executable file
12
stash/config/plugins/community/filenameParser/manifest
Executable file
@@ -0,0 +1,12 @@
|
||||
id: filenameParser
|
||||
name: Filename parser
|
||||
metadata:
|
||||
description: Parses filename into studio, date, performers and title
|
||||
version: 0.1-034a8a7
|
||||
date: "2025-05-18 19:37:19"
|
||||
requires: []
|
||||
source_repository: https://stashapp.github.io/CommunityScripts/stable/index.yml
|
||||
files:
|
||||
- README.md
|
||||
- filenameParser.yml
|
||||
- filenameParser.js
|
||||
Reference in New Issue
Block a user