// Common Patterns var patterns = { movieTitleAndYear: /(.+) \(\d{4}\)/, sceneTitleAndPerformers: /(.+) - ([A-zÀ-ú, ]+)/ } var rules = [ { name: 'Rule 1', pattern: [ 'Specific Studio', null, null ], fields: { studio: '#0', title: '#2', } }, { name: 'Rule 2', pattern: [ ['One Studio', 'Another Studio'], patterns.movieTitleAndYear, patterns.sceneTitleAndPerformers ], fields: { title: '#2', studio: '#0', performers: '#3' } }, ]; /* ---------------------------------------------------------------------------- // DO NOT EDIT BELOW! ---------------------------------------------------------------------------- */ function main() { try { switch (getTask(input.Args)) { case 'createTags': var runTag = getArg(input.Args, 'runTag'); var testTag = getArg(input.Args, 'testTag'); createTags([runTag, testTag]); break; case 'removeTags': var runTag = getArg(input.Args, 'runTag'); var testTag = getArg(input.Args, 'testTag'); removeTags([runTag, testTag]); break; case 'runRules': var runTag = getArg(input.Args, 'runTag'); initBasePaths(); runRules(runTag); break; case 'testRules': DEBUG = true; var testTag = getArg(input.Args, 'testTag'); initBasePaths(); runRules(testTag); break; case 'scene': var id = getId(input.Args); initBasePaths(); matchRuleWithSceneId(id, applyRule); break; case 'image': var id = getId(input.Args); initBasePaths(); break; default: throw 'Unsupported task'; } } catch (e) { return { Output: 'error', Error: e }; } return { Output: 'ok' }; } // Get an input arg function getArg(inputArgs, arg) { if (inputArgs.hasOwnProperty(arg)) { return inputArgs[arg]; } throw 'Input is missing ' + arg; } // Determine task based on input args function getTask(inputArgs) { if (inputArgs.hasOwnProperty('task')) { return inputArgs.task; } if (!inputArgs.hasOwnProperty('hookContext')) { return; } switch (inputArgs.hookContext.type) { case 'Scene.Create.Post': return 'scene'; case 'Image.Create.Post': return 'image'; } } // Get stash paths from configuration function initBasePaths() { var query ='\ query Query {\ configuration {\ general {\ stashes {\ path\ }\ }\ }\ }'; var result = gql.Do(query); if (!result.configuration) { throw 'Unable to get library paths'; } BASE_PATHS = result.configuration.general.stashes.map(function (stash) { return stash.path; }); if (BASE_PATHS == null || BASE_PATHS.length == 0) { throw 'Unable to get library paths'; } } // Create tag if it does not already exist function createTags(tags) { var query ='\ mutation TagCreate($input: TagCreateInput!) {\ tagCreate(input: $input) {\ id\ }\ }'; tags.forEach(function (tag) { if (tryGetTag(tag) !== null) { return; } var variables = { input: { name: tag } }; var result = gql.Do(query, variables); if (!result.tagCreate) { throw 'Could not create tag ' + tag; } }); } // Remove tags if it already exists function removeTags(tags) { tags.forEach(function (tag) { var tagId = tryGetTag(tag); if (tagId === null) { return; } var query = '\ mutation TagsDestroy($ids: [ID!]!) {\ tagsDestroy(ids: $ids)\ }'; var variables = { ids: [ tagId ] }; var result = gql.Do(query, variables); if (!result.tagsDestroy) { throw 'Unable to remove tag ' + tag; } }); } // Run rules for scenes containing tag function runRules(tag) { var tagId = tryGetTag(tag); if (tagId === null) { throw 'Tag ' + tag + ' does not exist'; } var query = '\ query FindScenes($sceneFilter: SceneFilterType) {\ findScenes(scene_filter: $sceneFilter) {\ scenes {\ id\ }\ }\ }'; var variables = { sceneFilter: { tags: { value: tagId, modifier: 'INCLUDES' } } }; var result = gql.Do(query, variables); if (!result.findScenes || result.findScenes.scenes.length == 0) { throw 'No scenes found with tag ' + tag; } result.findScenes.scenes.forEach(function (scene) { matchRuleWithSceneId(scene.id, applyRule); }); } // Get scene/image id from input args function getId(inputArgs) { if ((id = inputArgs.hookContext.id) == null) { throw 'Input is missing id'; } return id; } // Apply callback function to first matching rule for id function matchRuleWithSceneId(sceneId, cb) { var query = '\ query FindScene($findSceneId: ID) {\ findScene(id: $findSceneId) {\ files {\ path\ }\ }\ }'; var variables = { findSceneId: sceneId } var result = gql.Do(query, variables); if (!result.findScene || result.findScene.files.length == 0) { throw 'Missing scene for id: ' + sceneId; } for (var i = 0; i < result.findScene.files.length; i++) { try { matchRuleWithPath(sceneId, result.findScene.files[i].path, cb); if (DEBUG && bufferedOutput !== null && bufferedOutput !== '') { log.Info('[PathParser] ' + bufferedOutput); } return; } catch (e) { continue; } } if (DEBUG && bufferedOutput !== null && bufferedOutput !== '') { log.Info('[PathParser] ' + bufferedOutput); } throw 'No rule matches id: ' + sceneId; } // Apply callback to first matching rule for path function matchRuleWithPath(sceneId, path, cb) { // Remove base path for (var i = 0; i < BASE_PATHS.length; i++) { if (path.slice(0, BASE_PATHS[i].length) === BASE_PATHS[i]) { path = path.slice(BASE_PATHS[i].length); } } if (DEBUG) { bufferedOutput = path + '\n'; } // Split paths into parts var parts = path.split(/[\\/]/); // Remove extension from filename parts[parts.length - 1] = parts[parts.length - 1].slice(0, parts[parts.length - 1].lastIndexOf('.')); for (var i = 0; i < rules.length; i++) { var sceneData = testRule(rules[i].pattern, parts); if (sceneData !== null) { if (DEBUG) { bufferedOutput += 'Rule: ' + rules[i].name + '\n'; } log.Debug('[PathParser] Rule: ' + rules[i].name + '\nPath: ' + path); cb(sceneId, rules[i].fields, sceneData); return; } } bufferedOutput += 'No matching rule!'; throw 'No matching rule for path: ' + path; } // Test single rule function testRule(pattern, parts) { if (pattern.length !== parts.length) { return null; } var matchedParts = []; for (var i = 0; i < pattern.length; i++) { if ((subMatches = testPattern(pattern[i], parts[i])) == null) { return null; } matchedParts = [].concat(matchedParts, subMatches); } return matchedParts; } function testPattern(pattern, part) { // Match anything if (pattern == null) { return [part]; } // Simple match if (typeof pattern === 'string') { if (pattern === part) { return [part]; } return null; } // Predicate match if (typeof pattern == 'function') { try { var results = pattern(part); if (results !== null) { return results; } } catch (e) { throw e; } return null; } // Array match if (pattern instanceof Array) { for (var i = 0; i < pattern.length; i++) { if ((results = testPattern(pattern[i], part)) != null) { return results; } } return null; } // RegExp match if (pattern instanceof RegExp) { var results = pattern.exec(part); if (results === null) { return null; } return results.slice(1); } } // Apply rule function applyRule(sceneId, fields, data) { var any = false; var variables = { input: { id: sceneId } }; if (DEBUG) { for (var i = 0; i < data.length; i++) { bufferedOutput += '#' + i + ': ' + data[i] + '\n'; } } for (var field in fields) { var value = fields[field]; for (var i = data.length - 1; i >= 0; i--) { value = value.replace('#' + i, data[i]); } switch (field) { case 'title': if (DEBUG) { bufferedOutput += field + ': ' + value + '\n'; } variables.input['title'] = value; any = true; continue; case 'studio': var studioId = tryGetStudio(value); if (studioId == null) { continue; } if (DEBUG) { bufferedOutput += field + ': ' + value + '\n'; bufferedOutput += 'studio_id: ' + studioId + '\n'; } variables.input['studio_id'] = studioId; any = true; continue; case 'movie_title': var movie_title = value.split(' ').join('[\\W]*'); var movieId = tryGetMovie(movie_title); if (movieId == null) { continue; } if (!variables.input.hasOwnProperty('movies')) { variables.input['movies'] = [{}]; } if (DEBUG) { bufferedOutput += field + ': ' + value + '\n'; bufferedOutput += 'movie_id: ' + movieId + '\n'; } variables.input['movies'][0]['movie_id'] = movieId; any = true; continue; case 'scene_index': var sceneIndex = parseInt(value); if (isNaN(sceneIndex)) { continue; } if (!variables.input.hasOwnProperty('movies')) { variables.input['movies'] = [{}]; } if (DEBUG) { bufferedOutput += 'scene_index: ' + sceneIndex + '\n'; } variables.input['movies'][0]['scene_index'] = sceneIndex; continue; case 'performers': var performers = value.split(',').map(tryGetPerformer).filter(notNull); if (performers.length == 0) { continue; } if (DEBUG) { bufferedOutput += field + ': ' + value + '\n'; bufferedOutput += 'performer_ids: ' + performers.join(', ') + '\n'; } variables.input['performer_ids'] = performers; any = true; continue; case 'tags': var tags = value.split(',').map(tryGetTag).filter(notNull); if (tags.length == 0) { continue; } if (DEBUG) { bufferedOutput += field + ': ' + value + '\n'; bufferedOutput += 'tag_ids: ' + tags.join(', ') + '\n'; } variables.input['tag_ids'] = tags; any = true; continue; } } // Test only if (DEBUG) { if (!any) { bufferedOutput += 'No fields to update!\n'; } return; } // Remove movies if movie_id is missing if (variables.input.hasOwnProperty('movies') && !variables.input['movies'][0].hasOwnProperty('movie_id')) { delete variables.input['movies']; } // Apply updates var query = '\ mutation Mutation($input: SceneUpdateInput!) {\ sceneUpdate(input: $input) {\ id\ }\ }'; if (!any) { throw 'No fields to update for scene ' + sceneId; } var result = gql.Do(query, variables); if (!result.sceneUpdate) { throw 'Unable to update scene ' + sceneId; } } // Returns true for not null elements function notNull(ele) { return ele != null; } // Get studio id from studio name function tryGetStudio(studio) { var query = '\ query FindStudios($studioFilter: StudioFilterType) {\ findStudios(studio_filter: $studioFilter) {\ studios {\ id\ }\ count\ }\ }'; var variables = { studioFilter: { name: { value: studio.trim(), modifier: 'EQUALS' } } }; var result = gql.Do(query, variables); if (!result.findStudios || result.findStudios.count == 0) { return; } return result.findStudios.studios[0].id; } function tryGetMovie(movie_title) { var query = '\ query FindMovies($movieFilter: MovieFilterType) {\ findMovies(movie_filter: $movieFilter) {\ movies {\ id\ }\ count\ }\ }'; var variables = { movieFilter: { name: { value: movie_title.trim(), modifier: 'MATCHES_REGEX' } } }; var result = gql.Do(query, variables); if (!result.findMovies || result.findMovies.count == 0) { return; } return result.findMovies.movies[0].id; } // Get performer id from performer name function tryGetPerformer(performer) { var query = '\ query FindPerformers($performerFilter: PerformerFilterType) {\ findPerformers(performer_filter: $performerFilter) {\ performers {\ id\ }\ count\ }\ }'; var variables = { performerFilter: { name: { value: performer.trim(), modifier: 'EQUALS' } } }; var result = gql.Do(query, variables); if (!result.findPerformers || result.findPerformers.count == 0) { return; } return result.findPerformers.performers[0].id; } // Get tag id from tag name function tryGetTag(tag) { var query ='\ query FindTags($tagFilter: TagFilterType) {\ findTags(tag_filter: $tagFilter) {\ tags {\ id\ }\ count\ }\ }'; var variables = { tagFilter: { name: { value: tag.trim(), modifier: 'EQUALS' } } }; var result = gql.Do(query, variables); if (!result.findTags || result.findTags.count == 0) { return; } return result.findTags.tags[0].id; } var DEBUG = false; var BASE_PATHS = []; var bufferedOutput = ''; main();