From 464b48f6b0e7b8695d7814ced04e6c438f93c137 Mon Sep 17 00:00:00 2001 From: Casvt <88994465+Casvt@users.noreply.github.com> Date: Wed, 29 Nov 2023 15:16:28 +0100 Subject: [PATCH] Added Mass editor (#51) * Added base for mass editor * Added functionality for each action in backend --- backend/mass_edit.py | 81 +++++++++++++++ frontend/api.py | 38 ++++++- frontend/static/css/mass_editor.css | 131 +++++++++++++++++++++++++ frontend/static/js/mass_editor.js | 92 +++++++++++++++++ frontend/templates/add_volume.html | 1 + frontend/templates/library_import.html | 1 + frontend/templates/mass_editor.html | 113 +++++++++++++++++++++ frontend/templates/view_volume.html | 1 + frontend/templates/volumes.html | 1 + frontend/ui.py | 4 + 10 files changed, 462 insertions(+), 1 deletion(-) create mode 100644 backend/mass_edit.py create mode 100644 frontend/static/css/mass_editor.css create mode 100644 frontend/static/js/mass_editor.js create mode 100644 frontend/templates/mass_editor.html diff --git a/backend/mass_edit.py b/backend/mass_edit.py new file mode 100644 index 00000000..d2aad4e4 --- /dev/null +++ b/backend/mass_edit.py @@ -0,0 +1,81 @@ +#-*- coding: utf-8 -*- + +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING, List, Union + +from backend.custom_exceptions import InvalidKeyValue, VolumeDownloadedFor +from backend.db import get_db +from backend.naming import mass_rename +from backend.search import auto_search +from backend.volumes import Volume, refresh_and_scan + +if TYPE_CHECKING: + from backend.download_queue import DownloadHandler + +class MassEditorVariables: + download_handler: Union[None, DownloadHandler] = None + +def mass_editor_delete(volume_ids: List[int], **kwargs) -> None: + delete_volume_folder = kwargs.get('delete_folder', False) + if not isinstance(delete_volume_folder, bool): + raise InvalidKeyValue('delete_folder', delete_volume_folder) + + logging.info(f'Using mass editor, deleting volumes: {volume_ids}') + + for volume_id in volume_ids: + try: + Volume(volume_id).delete(delete_volume_folder) + except VolumeDownloadedFor: + continue + return + +def mass_editor_rename(volume_ids: List[int], **kwargs) -> None: + logging.info(f'Using mass editor, renaming volumes: {volume_ids}') + for volume_id in volume_ids: + mass_rename(volume_id) + return + +def mass_editor_update(volume_ids: List[int], **kwargs) -> None: + logging.info(f'Using mass editor, updating volumes: {volume_ids}') + for volume_id in volume_ids: + refresh_and_scan(volume_id) + return + +def mass_editor_search(volume_ids: List[int], **kwargs) -> None: + logging.info(f'Using mass editor, auto searching for volumes: {volume_ids}') + for volume_id in volume_ids: + search_results = auto_search(volume_id) + for result in search_results: + MassEditorVariables.download_handler.add( + result['link'], + volume_id + ) + return + +def mass_editor_convert(volume_ids: List[int], **kwargs) -> None: + logging.info(f'Using mass editor, converting for volumes: {volume_ids}') + return + +def mass_editor_unmonitor(volume_ids: List[int], **kwargs) -> None: + logging.info(f'Using mass editor, unmonitoring volumes: {volume_ids}') + for volume_id in volume_ids: + Volume(volume_id)._unmonitor() + return + +def mass_editor_monitor(volume_ids: List[int], **kwargs) -> None: + logging.info(f'Using mass editor, monitoring volumes: {volume_ids}') + for volume_id in volume_ids: + Volume(volume_id)._monitor() + return + +action_to_func = { + 'delete': mass_editor_delete, + 'rename': mass_editor_rename, + 'update': mass_editor_update, + 'search': mass_editor_search, + 'convert': mass_editor_convert, + 'unmonitor': mass_editor_unmonitor, + 'monitor': mass_editor_monitor +} diff --git a/frontend/api.py b/frontend/api.py index 54e2b4ad..8261eb25 100644 --- a/frontend/api.py +++ b/frontend/api.py @@ -1,7 +1,7 @@ #-*- coding: utf-8 -*- import logging -from typing import Any, Tuple +from typing import Any, Dict, List, Tuple from flask import Blueprint, Flask, request, send_file @@ -31,6 +31,7 @@ get_download_history) from backend.download_torrent_clients import TorrentClients, client_types from backend.library_import import import_library, propose_library_import +from backend.mass_edit import MassEditorVariables, action_to_func from backend.naming import (generate_volume_folder_name, mass_rename, preview_mass_rename) from backend.root_folders import RootFolders @@ -50,6 +51,7 @@ handler_context.teardown_appcontext(close_db) download_handler = DownloadHandler(handler_context) task_handler = TaskHandler(handler_context, download_handler) +MassEditorVariables.download_handler = download_handler def return_api(result: Any, error: str=None, code: int=200) -> Tuple[dict, int]: return {'error': error, 'result': result}, code @@ -796,3 +798,37 @@ def api_torrent_client(id: int): elif request.method == 'DELETE': client.delete() return return_api({}) + +#===================== +# Torrent Clients +#===================== +@api.route('/masseditor', methods=['POST']) +@error_handler +@auth +def api_mass_editor(): + data = request.get_json() + if not isinstance(data, dict): + raise InvalidKeyValue('body', data) + if not 'volume_ids' in data: + raise KeyNotFound('volume_ids') + if not 'action' in data: + raise KeyNotFound('action') + + action: str = data['action'] + volume_ids: List[int] = data['volume_ids'] + args: Dict[str, Any] = data.get('args', {}) + + if not ( + isinstance(volume_ids, list) + and all(isinstance(v, int) for v in volume_ids) + ): + raise InvalidKeyValue('volume_ids', volume_ids) + + if not action in action_to_func: + raise InvalidKeyValue('action', action) + + if not isinstance(args, dict): + raise InvalidKeyValue('args', args) + + action_to_func[action](volume_ids, **args) + return return_api({}) diff --git a/frontend/static/css/mass_editor.css b/frontend/static/css/mass_editor.css new file mode 100644 index 00000000..130a32e3 --- /dev/null +++ b/frontend/static/css/mass_editor.css @@ -0,0 +1,131 @@ +.nav-main > main > div { + flex: none; +} + +main { + color: var(--text-color); + padding: 1rem; +} + +#loading-window { + height: 100%; + display: flex; + justify-content: center; + align-items: center; +} + +#loading-window h2 { + font-size: clamp(1rem, 5vw, 2rem); + font-weight: 500; + text-align: center; +} + +#list-window { + height: 100%; + display: flex; + flex-direction: column; +} + +.action-bar { + display: flex; + flex-wrap: wrap; + gap: 1rem; + + padding: .5rem; + border-radius: 4px; + background-color: var(--tool-bar-color); + color: var(--light-color); +} + +.action-divider { + display: flex; + flex-wrap: wrap; + gap: 1rem; +} + +.action-bar button { + min-width: 5rem; + + border: 2px solid var(--border-color); + border-radius: 4px; + padding: .4rem .5rem; + color: var(--text-color); + background-color: var(--library-entry-color); + + transition: background-color 100ms linear; +} + +.action-bar button:hover { + background-color: var(--dark-hover-color); +} + +.option-container { + overflow: hidden; + border-radius: 4px; + border: 2px solid var(--border-color); + background-color: var(--library-entry-color); +} + +.option-container button { + border-radius: none; + border: none; +} + +.option-container select { + height: 100%; + background-color: var(--library-entry-color); + color: var(--text-color); +} + +.list-container { + position: relative; + + width: 100%; + overflow-x: auto; + + padding-top: 1rem; +} + +table { + width: max(100%, 30rem); + border-spacing: 0px; +} + +th { + text-align: left; +} + +th, +td { + padding: .5rem; + border-bottom: 1px solid var(--nav-color); +} + +th:first-child, +td:first-child { + width: 3rem; + padding-inline: 1rem; +} + +tbody > tr { + transition: background-color 150ms ease-in-out; +} + +tbody > tr:hover { + background-color: var(--dark-hover-color); +} + +@media (max-width: 820px) { + .action-bar { + justify-content: center; + } + .action-divider { + width: 100%; + } + .option-container { + flex: 1 1 auto; + } + .action-bar button { + flex: 1 1 0; + } +} diff --git a/frontend/static/js/mass_editor.js b/frontend/static/js/mass_editor.js new file mode 100644 index 00000000..27c681de --- /dev/null +++ b/frontend/static/js/mass_editor.js @@ -0,0 +1,92 @@ +const windows = { + loading: document.querySelector('#loading-window'), + list: document.querySelector('#list-window') +}; + +function fillVolumeList(api_key) { + document.querySelector('#selectall-input').checked = false; + const table = document.querySelector('.volume-list'); + table.innerHTML = ''; + fetch(`${url_base}/api/volumes?api_key=${api_key}`) + .then(response => response.json()) + .then(json => { + json.result.forEach(vol => { + const entry = document.createElement('tr'); + entry.dataset.id = vol.id; + + const select_container = document.createElement('td'); + const select = document.createElement('input'); + select.type = 'checkbox'; + select.checked = false; + select_container.appendChild(select); + entry.appendChild(select_container); + + const title = document.createElement('td'); + title.innerText = vol.title; + entry.appendChild(title); + + const year = document.createElement('td'); + year.innerText = vol.year; + entry.appendChild(year); + + const volume_number = document.createElement('td'); + volume_number.innerText = vol.volume_number; + entry.appendChild(volume_number); + + table.appendChild(entry); + }); + }); +}; + +function toggleSelection() { + const checked = document.querySelector('#selectall-input').checked; + document.querySelectorAll('.volume-list input[type="checkbox"]') + .forEach(c => c.checked = checked); +}; + +function runAction(api_key, action, args={}) { + windows.list.classList.add('hidden'); + windows.loading.classList.remove('hidden'); + + const volume_ids = [...document.querySelectorAll( + '.volume-list input[type="checkbox"]:checked' + )].map(v => parseInt(v.parentNode.parentNode.dataset.id)) + + fetch(`${url_base}/api/masseditor?api_key=${api_key}`, { + 'method': 'POST', + 'headers': {'Content-Type': 'application/json'}, + 'body': JSON.stringify({ + 'volume_ids': volume_ids, + 'action': action, + 'args': args + }) + }) + .then(response => { + fillVolumeList(api_key); + windows.loading.classList.add('hidden'); + windows.list.classList.remove('hidden'); + }); +}; + +// code run on load + +usingApiKey() +.then(api_key => { + fillVolumeList(api_key); + addEventListener('.action-bar > div > button', 'click', + e => runAction(api_key, e.target.dataset.action) + ); + addEventListener('button[data-action="delete"]', 'click', + e => runAction( + api_key, + e.target.dataset.action, + { + 'delete_folder': document.querySelector( + 'select[name="delete_folder"]' + ).value === "true" + } + ) + ); +}); + +addEventListener('#selectall-input', 'change', e => toggleSelection()); diff --git a/frontend/templates/add_volume.html b/frontend/templates/add_volume.html index 79a393d0..2c0b43e0 100644 --- a/frontend/templates/add_volume.html +++ b/frontend/templates/add_volume.html @@ -82,6 +82,7 @@

Volumes Add Volume Library Import + Mass Editor Activity Settings System diff --git a/frontend/templates/library_import.html b/frontend/templates/library_import.html index 5028772c..8950a8a8 100644 --- a/frontend/templates/library_import.html +++ b/frontend/templates/library_import.html @@ -62,6 +62,7 @@

Edit ComicVine Match

Volumes Add Volume Library Import + Mass Editor Activity Settings System diff --git a/frontend/templates/mass_editor.html b/frontend/templates/mass_editor.html new file mode 100644 index 00000000..d4ccd929 --- /dev/null +++ b/frontend/templates/mass_editor.html @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + Mass Editor - Kapowarr + + +
+
+
+

Edit ComicVine Match

+ +
+
+ +
+ + + + + + +
Search ResultAction
+
+
+
+
+ +
+ +
+ + + + \ No newline at end of file diff --git a/frontend/templates/view_volume.html b/frontend/templates/view_volume.html index dabe0d94..08afc92f 100644 --- a/frontend/templates/view_volume.html +++ b/frontend/templates/view_volume.html @@ -225,6 +225,7 @@

Convert volume files

Volumes Add Volume Library Import + Mass Editor Activity Settings System diff --git a/frontend/templates/volumes.html b/frontend/templates/volumes.html index 2c58be79..f536b2ab 100644 --- a/frontend/templates/volumes.html +++ b/frontend/templates/volumes.html @@ -45,6 +45,7 @@ Volumes Add Volume Library Import + Mass Editor Activity Settings System diff --git a/frontend/ui.py b/frontend/ui.py index 73105cdc..a40422f5 100644 --- a/frontend/ui.py +++ b/frontend/ui.py @@ -24,6 +24,10 @@ def ui_add_volume(): def ui_library_import(): return render_template('library_import.html', url_base=ui_vars['url_base']) +@ui.route('/mass-editor', methods=methods) +def ui_mass_editor(): + return render_template('mass_editor.html', url_base=ui_vars['url_base']) + @ui.route('/volumes/', methods=methods) def ui_view_volume(id): return render_template('view_volume.html', url_base=ui_vars['url_base'])