From a5224725cacbdfeeda09c32756db2c72c83e9a63 Mon Sep 17 00:00:00 2001 From: CasVT Date: Sat, 28 Oct 2023 16:54:12 +0200 Subject: [PATCH] Added "Import and Rename" (#22) --- backend/files.py | 2 +- backend/library_import.py | 79 ++++++++++++++++++++------ frontend/api.py | 10 +++- frontend/static/js/library_import.js | 16 ++++-- frontend/templates/library_import.html | 1 + 5 files changed, 83 insertions(+), 25 deletions(-) diff --git a/backend/files.py b/backend/files.py index f7769754..a7928c0e 100644 --- a/backend/files.py +++ b/backend/files.py @@ -591,7 +591,7 @@ def delete_empty_folders(top_folder: str, root_folder: str) -> None: if not top_folder.startswith(abspath(root_folder) + sep): logging.error(f'The folder {top_folder} is not in {root_folder}') return - + while (not exists(top_folder) or not( samefile(top_folder, root_folder) or listdir(top_folder) diff --git a/backend/library_import.py b/backend/library_import.py index 24cd0877..45f49758 100644 --- a/backend/library_import.py +++ b/backend/library_import.py @@ -2,7 +2,8 @@ import logging from asyncio import create_task, gather, run -from os.path import basename, commonpath, dirname, splitext +from os.path import basename, commonpath, dirname, join, splitext +from shutil import move from typing import Dict, List, Tuple, Union from aiohttp import ClientSession @@ -10,8 +11,10 @@ from backend.comicvine import ComicVine from backend.custom_exceptions import VolumeAlreadyAdded from backend.db import get_db -from backend.files import (_list_files, extract_filename_data, scan_files, +from backend.files import (_list_files, delete_empty_folders, + extract_filename_data, image_extensions, scan_files, supported_extensions) +from backend.naming import mass_rename from backend.root_folders import RootFolders from backend.search import _check_matching_titles from backend.volumes import Library, Volume @@ -124,40 +127,50 @@ def propose_library_import() -> List[dict]: return result -def import_library(matches: List[Dict[str, Union[str, int]]]) -> None: +def __find_lowest_common_folder(files: List[str]) -> str: + """Find the lowest folder that is shared between the files + + Args: + files (List[str]): The list of files to find the lowest common folder for + + Returns: + str: The path of the lowest common folder + """ + if len(files) == 1: + return dirname(files[0]) + + return commonpath(files) + +def import_library(matches: List[Dict[str, Union[str, int]]], rename_files: bool=False) -> None: """Add volume to library and import linked files Args: matches (List[Dict[str, Union[str, int]]]): List of dicts. The key `id` should supply the CV id of the volume and `filepath` the linked file. + rename_files (bool, optional): Should Kapowarr trigger a rename after importing files? Defaults to False. """ logging.info('Starting library import') - id_to_filepath = {} + cvid_to_filepath: Dict[int, List[str]] = {} for match in matches: - id_to_filepath.setdefault(match['id'], []).append(match['filepath']) - logging.debug(f'id_to_filepath: {id_to_filepath}') + cvid_to_filepath.setdefault(match['id'], []).append(match['filepath']) + logging.debug(f'id_to_filepath: {cvid_to_filepath}') root_folders = RootFolders().get_all() + cursor = get_db() library = Library() - for cv_id, files in id_to_filepath.items(): - # Find lowest common folder - volume_folder: str - if len(files) == 1: - volume_folder = dirname(files[0]) - else: - volume_folder = commonpath(files) + for cv_id, files in cvid_to_filepath.items(): + # Find lowest common folder (lcf) + volume_folder = __find_lowest_common_folder(files) if not rename_files else None - # Find root folder that lcf is in + # Find root folder that media is in for root_folder in root_folders: - if volume_folder.startswith(root_folder['folder']): + if files[0].startswith(root_folder['folder']): root_folder_id = root_folder['id'] break else: continue - logging.debug(f'{cv_id} -> {volume_folder}') - # Add volume if it isn't already try: volume_id = library.add( comicvine_id=str(cv_id), @@ -165,7 +178,37 @@ def import_library(matches: List[Dict[str, Union[str, int]]]) -> None: monitor=True, volume_folder=volume_folder ) - scan_files(Volume(volume_id).get_info()) + cursor.connection.commit() + except VolumeAlreadyAdded: + # The volume is already added but the file is not matched to it + # (it isn't because otherwise it wouldn't pop up in LI). + # That would mean that the file is actually not + # for that volume so skip. continue + + volume = Volume(volume_id) + if rename_files: + # Put files in volume folder + vf: str = volume.get_info()['folder'] + new_files = [] + for f in files: + if f.endswith(image_extensions): + new_files.append(move( + f, join(vf, basename(dirname(f))) + )) + else: + new_files.append(move(f, vf)) + + + delete_empty_folders(dirname(f), root_folder['folder']) + + scan_files(volume.get_info()) + + # Trigger rename + mass_rename(volume_id, filepath_filter=new_files) + + else: + scan_files(volume.get_info()) + return diff --git a/frontend/api.py b/frontend/api.py index b8f466d5..54179e3b 100644 --- a/frontend/api.py +++ b/frontend/api.py @@ -128,7 +128,7 @@ def extract_key(request, key: str, check_existence: bool=True) -> Any: if not value in blocklist_reasons: raise InvalidKeyValue(key, value) - elif key in ('monitor', 'delete_folder'): + elif key in ('monitor', 'delete_folder', 'rename_files'): if value == 'true': value = True elif value == 'false': @@ -150,6 +150,9 @@ def extract_key(request, key: str, check_existence: bool=True) -> Any: elif key == 'offset': value = 0 + elif key == 'rename_files': + value = False + return value #===================== @@ -357,12 +360,15 @@ def api_library_import(): elif request.method == 'POST': data = request.get_json() + rename_files = extract_key(request, 'rename_files', False) + if ( not isinstance(data, list) or not all(isinstance(e, dict) and 'filepath' in e and 'id' in e for e in data) ): raise InvalidKeyValue - import_library(data) + + import_library(data, rename_files) return return_api({}, code=201) #===================== diff --git a/frontend/static/js/library_import.js b/frontend/static/js/library_import.js index a962f4fe..cf9cc06d 100644 --- a/frontend/static/js/library_import.js +++ b/frontend/static/js/library_import.js @@ -48,6 +48,7 @@ function loadProposal(api_key) { document.querySelector('.table-container').classList.remove('hidden'); document.querySelector('#run-button').innerText = 'Run'; document.querySelector('#import-button').classList.remove('hidden'); + document.querySelector('#import-rename-button').classList.remove('hidden'); }); }; @@ -144,23 +145,29 @@ function searchCV() { }); }; -function importLibrary(api_key) { +function importLibrary(api_key, rename=false) { const import_button = document.querySelector('#import-button'); + const import_rename_button = document.querySelector('#import-rename-button'); + const used_button = rename ? import_rename_button : import_button; + const data = [...document.querySelectorAll('.proposal-list > tr:not([data-cv_id=""]) input[type="checkbox"]:checked')] .map(e => { return { 'filepath': e.parentNode.nextSibling.title, 'id': parseInt(e.parentNode.parentNode.dataset.cv_id) } }); - import_button.innerText = 'Importing'; - fetch(`${url_base}/api/libraryimport?api_key=${api_key}`, { + used_button.innerText = 'Importing'; + fetch(`${url_base}/api/libraryimport?api_key=${api_key}&rename_files=${rename}`, { 'method': 'POST', 'headers': {'Content-Type': 'application/json'}, 'body': JSON.stringify(data) }) .then(response => { + import_rename_button.innerText = 'Import and Rename'; + import_rename_button.classList.add('hidden'); import_button.innerText = 'Import'; import_button.classList.add('hidden'); + document.querySelector('.table-container').classList.add('hidden'); }); }; @@ -174,7 +181,8 @@ usingApiKey() loadProposal(api_key); }); addEventListener('#refresh-button', 'click', e => loadProposal(api_key)); - addEventListener('#import-button', 'click', e => importLibrary(api_key)); + addEventListener('#import-button', 'click', e => importLibrary(api_key, false)); + addEventListener('#import-rename-button', 'click', e => importLibrary(api_key, true)); }); setAttribute('.search-bar', 'action', 'javascript:searchCV();'); diff --git a/frontend/templates/library_import.html b/frontend/templates/library_import.html index f3409e66..b2de0ba2 100644 --- a/frontend/templates/library_import.html +++ b/frontend/templates/library_import.html @@ -82,6 +82,7 @@

Edit ComicVine Match

+