From da2a8bfe6e088a38fe220e4f195f20c7d7e35de3 Mon Sep 17 00:00:00 2001 From: Matt Blais Date: Fri, 27 Mar 2020 16:58:01 -0700 Subject: [PATCH 1/6] Require python3 --- xbsjsonedit | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) mode change 100755 => 100644 xbsjsonedit diff --git a/xbsjsonedit b/xbsjsonedit old mode 100755 new mode 100644 index 900d381..056c1c1 --- a/xbsjsonedit +++ b/xbsjsonedit @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # -*- coding: UTF-8 -*- """xBrowserSync json backup editor""" @@ -14,7 +14,7 @@ __version__ = "v0.3 191206" # # Issues, and changes pending # None -# +# #========================================================== import argparse @@ -45,7 +45,7 @@ def main(): global url_dict global folder_dict global tags_dict - + with io.open(args.Infile, encoding='utf8', errors="replace") as json_data: json_dict = json.load(json_data) @@ -95,11 +95,11 @@ Options: y: List empty folders (may arise if all children were deleted) Y: Delete empty folders - + w: Write out the bookmarks data to a file q: Quit/exit (Do a Write first!) """.format(term)) - + select = prompt("Enter option: ") if select == 'p': @@ -171,7 +171,7 @@ Options: json.dump(json_dict, ofile, ensure_ascii=False, indent=2) elif select == 'q': exit() - + else: print ("Invalid option {}".format(select)) @@ -186,7 +186,7 @@ def digin(parent, parent_id, path, search_term, operation, commit=False, indent= item_index = -1 ans = 'n' - + for item in parent: item_index += 1 @@ -194,7 +194,7 @@ def digin(parent, parent_id, path, search_term, operation, commit=False, indent= if operation == "printtree": print ("{:<4} > {}{}".format(item["id"], indent, item["title"])) - + if operation == "SearchFolders": if search_term in item["title"].lower(): print ("{:<4} > {}{}".format(item["id"], indent, item["title"])) @@ -243,7 +243,7 @@ def digin(parent, parent_id, path, search_term, operation, commit=False, indent= change_cnt += 1 if ans == 'q': return -1 - + if operation == "EmptyFolders": if len(item["children"]) == 0: print ("\n{:4} - {} >>> {}".format( @@ -359,12 +359,12 @@ def collect_items (path, parent, index): def delete_items (): """Delete queued-up items by collect_items. - Note: Items in lists that are to be deleted must be deleted from the highest + Note: Items in lists that are to be deleted must be deleted from the highest index / item number to the lowest, else the index numbers will be wrong, resulting in garbage. This is the purpose of the collect_items and delete_items functions. """ global path_dict - + for path in path_dict: xxx = path_dict[path]["index"] ## print path @@ -490,7 +490,7 @@ def prompt (prompt_text): # tags = "" # if "tags" in xxx: -# tags = xxx["tags"] +# tags = xxx["tags"] # return { # "title" : xxx["title"], @@ -499,7 +499,7 @@ def prompt (prompt_text): # "tags" : tags, # "text_full_path": text_full_path # } - + if __name__ == '__main__': parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawTextHelpFormatter) From 856f753949f0e07d40334d5f7c0cb985d17f1b7a Mon Sep 17 00:00:00 2001 From: Matt Blais Date: Fri, 27 Mar 2020 16:58:39 -0700 Subject: [PATCH 2/6] Initial commit --- LICENSE.txt | 0 README.md | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 LICENSE.txt mode change 100755 => 100644 README.md diff --git a/LICENSE.txt b/LICENSE.txt old mode 100755 new mode 100644 diff --git a/README.md b/README.md old mode 100755 new mode 100644 From 352b7aad423400008cc1304c68810129c3aef727 Mon Sep 17 00:00:00 2001 From: Matt Blais Date: Fri, 27 Mar 2020 17:02:46 -0700 Subject: [PATCH 3/6] Add unbuffered getch input and 'all' option --- xbsjsonedit | 60 ++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 55 insertions(+), 5 deletions(-) diff --git a/xbsjsonedit b/xbsjsonedit index 056c1c1..48abb74 100644 --- a/xbsjsonedit +++ b/xbsjsonedit @@ -30,6 +30,43 @@ INDENT = 2 OFILESUFFIX = "_OUT" NONE_SEARCH = "__NONE__" +# getch: Get one character from terminal without waiting for newline +class _Getch: + """Gets a single character from standard input. Does not echo to the screen. + """ + def __init__(self): + try: + self.impl = _GetchWindows() + except ImportError: + self.impl = _GetchUnix() + + def __call__(self): return self.impl() + +class _GetchUnix: + def __init__(self): + import tty, sys + + def __call__(self): + import sys, tty, termios + fd = sys.stdin.fileno() + old_settings = termios.tcgetattr(fd) + try: + tty.setraw(sys.stdin.fileno()) + ch = sys.stdin.read(1) + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) + return ch + +class _GetchWindows: + def __init__(self): + import msvcrt + + def __call__(self): + import msvcrt + return msvcrt.getch() + +getch = _Getch() + # Global items url_dict = {} @@ -100,8 +137,9 @@ Options: q: Quit/exit (Do a Write first!) """.format(term)) - select = prompt("Enter option: ") - + print("Enter option: ", end='', flush=True) + select = getch() + print( select ) if select == 'p': digin(parent=json_dict["xbrowsersync"]["data"]["bookmarks"], parent_id="", path="/", search_term="", operation="printtree") @@ -427,6 +465,8 @@ def dup_urls (commit=False): global match_cnt global change_cnt global path_dict + global yes_all + yes_all = False for url in url_dict: if len(url_dict[url]["instance"]) > 1: print ("-------------------------------------------------\n{}".format(url)) @@ -435,10 +475,20 @@ def dup_urls (commit=False): match_cnt += 1 if commit: print ("") + first_instance = True for instance in url_dict[url]["instance"]: - print (" {:>4} - {}".format(instance["id"], instance["path"])) - ans = prompt ("Confirm delete for this item ('y'es, 'q'uit, 's'kip to next URL - default no) ").lower() - if ans == 'y': + if first_instance and yes_all: + first_instance = False + continue # When yes_all is active, always keep first instance of duplicate bookmarks + first_instance = False + if not yes_all: + print (" {:>4} - {}".format(instance["id"], instance["path"])) + print ("Confirm delete for this item ('y'es, 'q'uit, 's'kip to next URL, 'a'll - default no): ", end='', flush=True) + ans = getch() + print (ans) + if ans == 'a': + yes_all = True + if ans == 'y' or yes_all: collect_items (instance["path"], instance["parent"], instance["item_index"]) change_cnt += 1 if ans == 's': From d84314ed1ad0ca7ca0ee24b139bee5289682e1b8 Mon Sep 17 00:00:00 2001 From: cjnaz Date: Mon, 30 Mar 2020 21:15:52 -0700 Subject: [PATCH 4/6] working1 --- xbsjsonedit | 206 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 127 insertions(+), 79 deletions(-) mode change 100644 => 100755 xbsjsonedit diff --git a/xbsjsonedit b/xbsjsonedit old mode 100644 new mode 100755 index 48abb74..fa9491d --- a/xbsjsonedit +++ b/xbsjsonedit @@ -2,12 +2,13 @@ # -*- coding: UTF-8 -*- """xBrowserSync json backup editor""" -__version__ = "v0.3 191206" +__version__ = "v0.4 200329" #========================================================== # # Chris Nelson 2018 - 2019 # +# 200329 v0.4 Merged pull request #3 from mblais - add unbuffered getch input; add 'all' option # 191206 v0.3 Added tags dump # 191205 v0.2 Updated to xbrowsersync v1.5 and Python 3.x ONLY # 181128 v0.1 New @@ -30,44 +31,6 @@ INDENT = 2 OFILESUFFIX = "_OUT" NONE_SEARCH = "__NONE__" -# getch: Get one character from terminal without waiting for newline -class _Getch: - """Gets a single character from standard input. Does not echo to the screen. - """ - def __init__(self): - try: - self.impl = _GetchWindows() - except ImportError: - self.impl = _GetchUnix() - - def __call__(self): return self.impl() - -class _GetchUnix: - def __init__(self): - import tty, sys - - def __call__(self): - import sys, tty, termios - fd = sys.stdin.fileno() - old_settings = termios.tcgetattr(fd) - try: - tty.setraw(sys.stdin.fileno()) - ch = sys.stdin.read(1) - finally: - termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) - return ch - -class _GetchWindows: - def __init__(self): - import msvcrt - - def __call__(self): - import msvcrt - return msvcrt.getch() - -getch = _Getch() - - # Global items url_dict = {} folder_dict = {} @@ -88,18 +51,20 @@ def main(): term = NONE_SEARCH - if args.Print: + if args.print: digin(parent=json_dict["xbrowsersync"]["data"]["bookmarks"], parent_id="", path="/", search_term="", operation="printtree") exit() - if args.Tags_Print: + if args.tags_print: digin(parent=json_dict["xbrowsersync"]["data"]["bookmarks"], parent_id="", path="/", search_term="", operation="GatherTags") for tag in sorted(tags_dict.keys()): - if args.Tags_Count == 0: + if args.tags_count == 0: if len(tags_dict[tag]) == 1: print ("\n[{}]:".format(tag)) print (" {:<4} - {}".format(tags_dict[tag][0]["id"], tags_dict[tag][0]["title"])) + elif args.tags_count == -1: + print ("{:5} {}".format(len(tags_dict[tag]), tag)) else: - if len(tags_dict[tag]) >= args.Tags_Count: + if len(tags_dict[tag]) >= args.tags_count: print ("\n[{}]:".format(tag)) for bookmark in tags_dict[tag]: print (" {:<4} - {}".format(bookmark["id"], bookmark["title"])) @@ -137,14 +102,15 @@ Options: q: Quit/exit (Do a Write first!) """.format(term)) - print("Enter option: ", end='', flush=True) - select = getch() - print( select ) + select = prompt("Enter option: ", valid="stTgGdDfFxXyYwq") + # print("Enter option: ", end='', flush=True) + # select = getch() + # print( select ) if select == 'p': digin(parent=json_dict["xbrowsersync"]["data"]["bookmarks"], parent_id="", path="/", search_term="", operation="printtree") elif select == 's': - term = prompt("Search for (empty to clear search term): ").lower() + term = prompt_long("Search for (empty to clear search term): ").lower() if term == "": term = NONE_SEARCH elif select == 't': @@ -202,16 +168,24 @@ Options: print ("\nMatches: {} Deletes: {}\n".format(match_cnt, change_cnt)) elif select == 'w': - ans = prompt("Output file name (default <{}>: ".format(args.Infile + OFILESUFFIX)) + ans = prompt_long("Output file name (default <{}>: ".format(args.Infile + OFILESUFFIX)) if ans == "": ans = args.Infile + OFILESUFFIX with io.open(ans, "w", encoding='utf8') as ofile: json.dump(json_dict, ofile, ensure_ascii=False, indent=2) elif select == 'q': exit() - + # if not changes: + # exit() + # else: + # if prompt("Exit without saving changes?", valid="yn") == 'y': + # exit() else: - print ("Invalid option {}".format(select)) + print ("Shouldn't have gotten here!") + exit() + + # else: + # print ("Invalid option {}".format(select)) @@ -221,6 +195,7 @@ def digin(parent, parent_id, path, search_term, operation, commit=False, indent= global change_cnt global do_all global path_dict + # global changes item_index = -1 ans = 'n' @@ -234,6 +209,7 @@ def digin(parent, parent_id, path, search_term, operation, commit=False, indent= print ("{:<4} > {}{}".format(item["id"], indent, item["title"])) if operation == "SearchFolders": + # local_changes = False if search_term in item["title"].lower(): print ("{:<4} > {}{}".format(item["id"], indent, item["title"])) match_cnt += 1 @@ -241,17 +217,24 @@ def digin(parent, parent_id, path, search_term, operation, commit=False, indent= if do_all: ans = 'y' else: - ans = prompt("Confirm delete for this item ('y'es, 'a'll, or 'q'uit, default no) ").lower() + ans = prompt("Confirm delete for this item ('y'es, 'a'll, or 'q'uit, default 'n'o) ", valid="yaqn\r") + # print("Enter option: ", end='', flush=True) + # select = getch() + # print( select ) + if ans == 'a': do_all = True ans = 'y' if ans == 'y': collect_items (path + parent_id, parent, item_index) change_cnt += 1 + # local_changes = True if ans == 'q': - if prompt("Discard pending deletes ('y'es, default no)? ").lower() == 'y': + if prompt("Discard pending deletes ('y'es, default 'n'o)? ", valid="yn\n") == 'y': + # local_changes = False path_dict = {} return -1 + # changes = changes or local_changes @@ -272,13 +255,14 @@ def digin(parent, parent_id, path, search_term, operation, commit=False, indent= if do_all: ans = 'y' else: - ans = prompt("Confirm immediate delete of tags on this folder ('y'es, 'a'll, or 'q'uit, default no) ").lower() + ans = prompt("Confirm immediate delete of tags on this folder ('y'es, 'a'll, or 'q'uit, default 'n'o) ", valid="yaqn\r") if ans == 'a': do_all = True ans = 'y' if ans == 'y': del item["tags"] change_cnt += 1 + # changes = True if ans == 'q': return -1 @@ -293,7 +277,7 @@ def digin(parent, parent_id, path, search_term, operation, commit=False, indent= if do_all: ans = 'y' else: - ans = prompt("Confirm delete of this empty folder ('y'es, 'a'll, or 'q'uit, default no) ").lower() + ans = prompt("Confirm delete of this empty folder ('y'es, 'a'll, or 'q'uit, default 'n'o) ", valid="yaqn\r") if ans == 'a': do_all = True ans = 'y' @@ -301,7 +285,7 @@ def digin(parent, parent_id, path, search_term, operation, commit=False, indent= collect_items (path + parent_id, parent, item_index) change_cnt += 1 if ans == 'q': - if prompt("Discard pending deletes ('y'es, default no)? ").lower() == 'y': + if prompt("Discard pending deletes ('y'es, default 'n'o) ", valid="yn\r") == 'y': path_dict = {} # TODO return -1 @@ -359,7 +343,7 @@ def digin(parent, parent_id, path, search_term, operation, commit=False, indent= if do_all: ans = 'y' else: - ans = prompt("Confirm delete for this item ('y'es, 'a'll, or 'q'uit, default no) ").lower() + ans = prompt("Confirm delete for this item ('y'es, 'a'll, or 'q'uit, default 'n'o) ", valid="yaqn\r") if ans == 'a': do_all = True ans = 'y' @@ -367,7 +351,7 @@ def digin(parent, parent_id, path, search_term, operation, commit=False, indent= collect_items (path + parent_id, parent, item_index) change_cnt += 1 if ans == 'q': - if prompt("Discard pending deletes ('y'es, default no)? ").lower() == 'y': + if prompt("Discard pending deletes ('y'es, default 'n'o) ", valid="yn\r") == 'y': path_dict = {} return -1 @@ -429,24 +413,34 @@ def dup_folders (commit=False): global change_cnt global folder_dict global path_dict + yes_all = False for folder in folder_dict: if len(folder_dict[folder]["instance"]) > 1: print ("-------------------------------------------------\n{}".format(folder)) - for instance in folder_dict[folder]["instance"]: + # for instance in folder_dict[folder]["instance"]: + for instance in sorted(folder_dict[folder]["instance"], key=lambda k: k['id']): # Sort id numbers so that lowest is kept for 'a'll mode print (" {:>4} - {}".format(instance["id"], instance["path"])) match_cnt += 1 if commit: print ("") - for instance in folder_dict[folder]["instance"]: - print (" {:>4} - {}".format(instance["id"], instance["path"])) - ans = prompt ("Confirm delete for this item ('y'es, 'q'uit, 's'kip to next URL - default no) ").lower() - if ans == 'y': + first_instance = True + for instance in sorted(folder_dict[folder]["instance"], key=lambda k: k['id']): + if first_instance and yes_all: + first_instance = False + continue # When yes_all is active, always keep first instance of duplicate folders + first_instance = False + if not yes_all: + print (" {:>4} - {}".format(instance["id"], instance["path"])) + ans = prompt ("Confirm delete for this item ('y'es, 'q'uit, 's'kip to next URL, 'a'll - default 'n'o) ", valid="yqsan\r") + if ans == 'a': + yes_all = True + if ans == 'y' or yes_all: collect_items (instance["path"], instance["parent"], instance["item_index"]) change_cnt += 1 if ans == 's': break if ans == 'q': - if prompt("Discard pending deletes ('y'es, default no)? ").lower() == 'y': + if prompt("Discard pending deletes ('y'es, default 'n'o) ", valid="yn\r") == 'y': path_dict = {} return -1 @@ -465,27 +459,30 @@ def dup_urls (commit=False): global match_cnt global change_cnt global path_dict - global yes_all + # global yes_all yes_all = False for url in url_dict: if len(url_dict[url]["instance"]) > 1: print ("-------------------------------------------------\n{}".format(url)) - for instance in url_dict[url]["instance"]: + # for instance in url_dict[url]["instance"]: + for instance in sorted(url_dict[url]["instance"], key=lambda k: k['id']): # Sort id numbers so that lowest is kept for 'a'll mode print (" {:>4} - {}".format(instance["id"], instance["path"])) match_cnt += 1 if commit: print ("") first_instance = True - for instance in url_dict[url]["instance"]: + # for instance in url_dict[url]["instance"]: + for instance in sorted(url_dict[url]["instance"], key=lambda k: k['id']): if first_instance and yes_all: first_instance = False continue # When yes_all is active, always keep first instance of duplicate bookmarks first_instance = False if not yes_all: print (" {:>4} - {}".format(instance["id"], instance["path"])) - print ("Confirm delete for this item ('y'es, 'q'uit, 's'kip to next URL, 'a'll - default no): ", end='', flush=True) - ans = getch() - print (ans) + # print ("Confirm delete for this item ('y'es, 'q'uit, 's'kip to next URL, 'a'll - default no): ", end='', flush=True) + ans = prompt ("Confirm delete for this item ('y'es, 'q'uit, 's'kip to next URL, 'a'll - default 'n'o) ", valid="yqsan\r") + # ans = getch() + # print (ans) if ans == 'a': yes_all = True if ans == 'y' or yes_all: @@ -494,17 +491,68 @@ def dup_urls (commit=False): if ans == 's': break if ans == 'q': - if prompt("Discard pending deletes ('y'es, default no)? ").lower() == 'y': + if prompt("Discard pending deletes ('y'es, default 'n'o) ", valid="yn\r") == 'y': path_dict = {} return -1 +def prompt_long (prompt_text): + return (input(prompt_text)) -def prompt (prompt_text): - # if sys.version_info[0] < 3: - # return (raw_input(prompt_text)) - # else: - return (input(prompt_text)) +def prompt (prompt_text, valid=None): + print(prompt_text, end='', flush=True) + select = getch() + if valid is not None: + while select not in valid: + xx = list(valid) + yy = ", ".join(xx).replace("\r", "enter") + print("\nInvalid input - expecting one of <{}>: ".format(yy), end='', flush=True) + # print("\nInvalid input - expecting one of <{}>: ".format(valid.replace("\r","\\r")), end='', flush=True) + # print("\nInvalid input - expecting one of <{}>: ".format(valid.replace("\n","\\n")), end='', flush=True) + select = getch() + # print ("<",ord(select),">") + # print ("<",ord("\n"),">") + + print(select) + return select + + + +class _Getch: + """Gets a single character from standard input. Does not echo to the screen. + """ + def __init__(self): + try: + self.impl = _GetchWindows() + except ImportError: + self.impl = _GetchUnix() + + def __call__(self): return self.impl() + +class _GetchUnix: + def __init__(self): + import tty, sys + + def __call__(self): + import sys, tty, termios + fd = sys.stdin.fileno() + old_settings = termios.tcgetattr(fd) + try: + tty.setraw(sys.stdin.fileno()) + ch = sys.stdin.read(1) + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) + return ch + +class _GetchWindows: + def __init__(self): + import msvcrt + + def __call__(self): + import msvcrt + return msvcrt.getch() + +getch = _Getch() # def get_item (path): @@ -555,12 +603,12 @@ if __name__ == '__main__': parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawTextHelpFormatter) parser.add_argument('Infile', help="json backup file") - parser.add_argument('--Print', '-p', action='store_true', + parser.add_argument('--print', '-p', action='store_true', help="Print bookmark hierarchy (redirect to less or a file)") - parser.add_argument('--Tags-Print', '-t', action='store_true', + parser.add_argument('--tags-print', '-t', action='store_true', help="Print tags list") - parser.add_argument('--Tags-Count', '-c', default=2, type=int, - help="Filter Tags-List for min number of times a tag is used (defult 2). =0 prints only single use tags.") + parser.add_argument('--tags-count', '-c', default=2, type=int, + help="Filter tags-print for min number of times a tag is used (default 2). =0 prints only single use tags. =-1 prints only count per tag") parser.add_argument('-V', '--version', help="Return version number and exit.", action='version', From eb828b0f9d9f6a98e7daabbc82087ee819a7d8e8 Mon Sep 17 00:00:00 2001 From: cjnaz Date: Mon, 30 Mar 2020 23:05:06 -0700 Subject: [PATCH 5/6] v1.0 final --- README.md | 37 +++++++++------------- xbsjsonedit | 88 ++++++++++++++++------------------------------------- 2 files changed, 40 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index 6302dca..07aa5a5 100644 --- a/README.md +++ b/README.md @@ -9,21 +9,18 @@ If you want it to do other functions, grab the code and go for it. ## Usage ``` $ ./xbsjsonedit -h -usage: xbsjsonedit [-h] [--Print] [--Tags-Print] [--Tags-Count TAGS_COUNT] - [-V] - Infile +usage: xbsjsonedit [-h] [--print] [--tags TAGS] [--names] [-V] Infile xBrowserSync json backup editor positional arguments: - Infile json backup file + Infile json backup file. optional arguments: -h, --help show this help message and exit - --Print, -p Print bookmark hierarchy (redirect to less or a file) - --Tags-Print, -t Print tags list - --Tags-Count TAGS_COUNT, -c TAGS_COUNT - Filter Tags-List for min number of times a tag is used (defult 2). =0 prints only single use tags. + --print, -p Print bookmark hierarchy (redirect to less or a file). + --tags TAGS, -t TAGS Print tags list filtered by tags with a minimum number of uses. n=0 prints only single use tags. + --names, -n Used with --tags to enable printing the titles/names for each tag. -V, --version Return version number and exit. ``` @@ -69,35 +66,29 @@ Five modes are supported Listing/Deleting based on: The Print function prints the full list of bookmarks in folder hierarchical form, listing the ID number and Title of each folder and bookmark. The `--Print` switch may be used for getting a good visual dump of the bookmarks in less or an editor for reference while using the interactive mode. The tags on each bookmark are listed as well. -The `--Tags-Print` switch prints bookmarks by tag name, where a minimum of 2 bookmarks share the same tag, as set by the `--Tags-Count` switch. Setting `--Tags-Count 1` prints all bookmarks with tags. Setting `--Tags-Count 0` prints all bookmarks with only one tag - useful for finding remnant tags. +The `--tags ` switch prints each tag where the number of bookmarks with that tag >= ``. Optionally include the `--names` switch to list the bookmark titles/names for each tag. `--tags 0` is a special case that prints only tags that have exactly one occurrence, which may be useful for finding extraneous/remnant tags usage. A lower case letter `t/g/d/f/x/y` will list the offenders, while an upper case letter -`T/G/D/F/X/Y` will allow for selective deletes of the offenders. The lower case List operation -need not be run before running the upper case Delete operation. +`T/G/D/F/X/Y` will allow for selective deletes of the offenders. The lower case List operation need not be run before running the upper case Delete operation. Delete functions support individual bookmark selection, or all that match the selection mode. -You cannot do any damage to the original bookmark file during a session, so experiment and poke -around. +You cannot do any damage to the original bookmark file during a session, so experiment and poke around. When deleting empty folders you may find that you need to repeat the operation a few times. -This happens when there are folders within folders that contain no bookmarks. The lower folder -must be deleted before the next higher folder is seen as empty. Just repeat the folder deletes -until the Matches count is 0. +This happens when there are folders within folders that contain no bookmarks. The lower folder must be deleted before the next higher folder is seen as empty. Just repeat the folder deletes until the Matches count is 0. -The output file defaults to the Infile + "_OUT", and can be specified during the write operation. -You may want to write out intermediate editing results to save work done up to that point. -The output format is "pretty printed" json for readability, and is accepted by the -xBrowserSync app Restore function via copy/paste. Note: xBrowserSync will allow you to restore -to (blast) your current -syncID, so you may want to Disable Sync, then create a new sync before restoring the modified bookmarks. **NOTE** that tags are not restored if sync is disabled. To recover, simply enable sync and do another restore. +The output file defaults to the Infile + "_OUT", and can be specified during the write operation. You may want to write out intermediate editing results to save work done up to that point. The output format is "pretty printed" json for readability, and is accepted by the xBrowserSync app Restore function via copy/paste. +- **Note** [_may no longer be the case_]: xBrowserSync will allow you to restore to (blast) your current syncID, so you may want to Disable Sync, then create a new sync before restoring the modified bookmarks. +- **NOTE** that tags are not restored by xBrowserSync if sync is disabled. To recover, simply enable sync and do another restore. ## Known issues: -- This code only works with Python 3. Development and testing was done on Linux with Python 3.7.3. +- This code only works with Python 3. Development and testing was done on Linux with Python 3.7.3. Limited testing was done on Windows with Python 3.8.0. ## Version history +- 200329 v1.0 Merged pull request #3 from mblais (add unbuffered getch input; add 'all' option), Reworked --tags and --names switches. - 191206 v0.3 Added tags dump - 191205 v0.2 Updated to xbrowsersync v1.5 and Python 3.x ONLY - 181128 v0.1 New \ No newline at end of file diff --git a/xbsjsonedit b/xbsjsonedit index fa9491d..a17cdc4 100755 --- a/xbsjsonedit +++ b/xbsjsonedit @@ -1,14 +1,13 @@ #!/usr/bin/env python3 -# -*- coding: UTF-8 -*- """xBrowserSync json backup editor""" -__version__ = "v0.4 200329" +__version__ = "v1.0 200330" #========================================================== # # Chris Nelson 2018 - 2019 # -# 200329 v0.4 Merged pull request #3 from mblais - add unbuffered getch input; add 'all' option +# 200330 v1.0 Merged pull request #3 from mblais (add unbuffered getch input; add 'all' option), Reworked --tags and --names switches. # 191206 v0.3 Added tags dump # 191205 v0.2 Updated to xbrowsersync v1.5 and Python 3.x ONLY # 181128 v0.1 New @@ -54,20 +53,22 @@ def main(): if args.print: digin(parent=json_dict["xbrowsersync"]["data"]["bookmarks"], parent_id="", path="/", search_term="", operation="printtree") exit() - if args.tags_print: + if args.tags > -1: digin(parent=json_dict["xbrowsersync"]["data"]["bookmarks"], parent_id="", path="/", search_term="", operation="GatherTags") for tag in sorted(tags_dict.keys()): - if args.tags_count == 0: + if args.tags == 0: if len(tags_dict[tag]) == 1: - print ("\n[{}]:".format(tag)) - print (" {:<4} - {}".format(tags_dict[tag][0]["id"], tags_dict[tag][0]["title"])) - elif args.tags_count == -1: - print ("{:5} {}".format(len(tags_dict[tag]), tag)) + print ("[{}] (1):".format(tag)) + if args.names: + print (" {:<4} - {}".format(tags_dict[tag][0]["id"], tags_dict[tag][0]["title"])) + print() else: - if len(tags_dict[tag]) >= args.tags_count: - print ("\n[{}]:".format(tag)) - for bookmark in tags_dict[tag]: - print (" {:<4} - {}".format(bookmark["id"], bookmark["title"])) + if len(tags_dict[tag]) >= args.tags: + print ("[{}] ({}):".format(tag, len(tags_dict[tag]))) + if args.names: + for bookmark in tags_dict[tag]: + print (" {:<4} - {}".format(bookmark["id"], bookmark["title"])) + print() exit() while (1): @@ -103,9 +104,6 @@ Options: """.format(term)) select = prompt("Enter option: ", valid="stTgGdDfFxXyYwq") - # print("Enter option: ", end='', flush=True) - # select = getch() - # print( select ) if select == 'p': digin(parent=json_dict["xbrowsersync"]["data"]["bookmarks"], parent_id="", path="/", search_term="", operation="printtree") @@ -174,20 +172,12 @@ Options: with io.open(ans, "w", encoding='utf8') as ofile: json.dump(json_dict, ofile, ensure_ascii=False, indent=2) elif select == 'q': - exit() - # if not changes: - # exit() - # else: - # if prompt("Exit without saving changes?", valid="yn") == 'y': - # exit() + if prompt("Quit now? (Remember to write any changes first!) ", valid="yn") == 'y': + exit() else: print ("Shouldn't have gotten here!") exit() - # else: - # print ("Invalid option {}".format(select)) - - def digin(parent, parent_id, path, search_term, operation, commit=False, indent=""): """Recurse through the json dictionary bookmark tree, with operation options.""" @@ -195,7 +185,6 @@ def digin(parent, parent_id, path, search_term, operation, commit=False, indent= global change_cnt global do_all global path_dict - # global changes item_index = -1 ans = 'n' @@ -218,25 +207,16 @@ def digin(parent, parent_id, path, search_term, operation, commit=False, indent= ans = 'y' else: ans = prompt("Confirm delete for this item ('y'es, 'a'll, or 'q'uit, default 'n'o) ", valid="yaqn\r") - # print("Enter option: ", end='', flush=True) - # select = getch() - # print( select ) - if ans == 'a': do_all = True ans = 'y' if ans == 'y': collect_items (path + parent_id, parent, item_index) change_cnt += 1 - # local_changes = True if ans == 'q': if prompt("Discard pending deletes ('y'es, default 'n'o)? ", valid="yn\n") == 'y': - # local_changes = False path_dict = {} return -1 - # changes = changes or local_changes - - if operation == "FolderTags": if "tags" in item: @@ -262,7 +242,6 @@ def digin(parent, parent_id, path, search_term, operation, commit=False, indent= if ans == 'y': del item["tags"] change_cnt += 1 - # changes = True if ans == 'q': return -1 @@ -389,11 +368,8 @@ def delete_items (): for path in path_dict: xxx = path_dict[path]["index"] -## print path -## print xxx xxx.sort(reverse=True) for _index in xxx: -## print path_dict[path]["parent"] del path_dict[path]["parent"][_index] path_dict = {} @@ -417,7 +393,6 @@ def dup_folders (commit=False): for folder in folder_dict: if len(folder_dict[folder]["instance"]) > 1: print ("-------------------------------------------------\n{}".format(folder)) - # for instance in folder_dict[folder]["instance"]: for instance in sorted(folder_dict[folder]["instance"], key=lambda k: k['id']): # Sort id numbers so that lowest is kept for 'a'll mode print (" {:>4} - {}".format(instance["id"], instance["path"])) match_cnt += 1 @@ -459,19 +434,16 @@ def dup_urls (commit=False): global match_cnt global change_cnt global path_dict - # global yes_all yes_all = False for url in url_dict: if len(url_dict[url]["instance"]) > 1: print ("-------------------------------------------------\n{}".format(url)) - # for instance in url_dict[url]["instance"]: for instance in sorted(url_dict[url]["instance"], key=lambda k: k['id']): # Sort id numbers so that lowest is kept for 'a'll mode print (" {:>4} - {}".format(instance["id"], instance["path"])) match_cnt += 1 if commit: print ("") first_instance = True - # for instance in url_dict[url]["instance"]: for instance in sorted(url_dict[url]["instance"], key=lambda k: k['id']): if first_instance and yes_all: first_instance = False @@ -479,10 +451,7 @@ def dup_urls (commit=False): first_instance = False if not yes_all: print (" {:>4} - {}".format(instance["id"], instance["path"])) - # print ("Confirm delete for this item ('y'es, 'q'uit, 's'kip to next URL, 'a'll - default no): ", end='', flush=True) ans = prompt ("Confirm delete for this item ('y'es, 'q'uit, 's'kip to next URL, 'a'll - default 'n'o) ", valid="yqsan\r") - # ans = getch() - # print (ans) if ans == 'a': yes_all = True if ans == 'y' or yes_all: @@ -495,10 +464,11 @@ def dup_urls (commit=False): path_dict = {} return -1 + + def prompt_long (prompt_text): return (input(prompt_text)) - def prompt (prompt_text, valid=None): print(prompt_text, end='', flush=True) select = getch() @@ -507,17 +477,10 @@ def prompt (prompt_text, valid=None): xx = list(valid) yy = ", ".join(xx).replace("\r", "enter") print("\nInvalid input - expecting one of <{}>: ".format(yy), end='', flush=True) - # print("\nInvalid input - expecting one of <{}>: ".format(valid.replace("\r","\\r")), end='', flush=True) - # print("\nInvalid input - expecting one of <{}>: ".format(valid.replace("\n","\\n")), end='', flush=True) select = getch() - # print ("<",ord(select),">") - # print ("<",ord("\n"),">") - print(select) return select - - class _Getch: """Gets a single character from standard input. Does not echo to the screen. """ @@ -550,7 +513,8 @@ class _GetchWindows: def __call__(self): import msvcrt - return msvcrt.getch() + # return msvcrt.getch() + return msvcrt.getch().decode("utf-8") getch = _Getch() @@ -602,13 +566,13 @@ getch = _Getch() if __name__ == '__main__': parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawTextHelpFormatter) parser.add_argument('Infile', - help="json backup file") + help="json backup file.") parser.add_argument('--print', '-p', action='store_true', - help="Print bookmark hierarchy (redirect to less or a file)") - parser.add_argument('--tags-print', '-t', action='store_true', - help="Print tags list") - parser.add_argument('--tags-count', '-c', default=2, type=int, - help="Filter tags-print for min number of times a tag is used (default 2). =0 prints only single use tags. =-1 prints only count per tag") + help="Print bookmark hierarchy (redirect to less or a file).") + parser.add_argument('--tags', '-t', type=int, default=-1, + help="Print tags list filtered by tags with a minimum number of uses. n=0 prints only single use tags.") + parser.add_argument('--names', '-n', action='store_true', + help="Used with --tags to enable printing the titles/names for each tag.") parser.add_argument('-V', '--version', help="Return version number and exit.", action='version', From 9425caece7a600412cbea1131ab325fb2bd26591 Mon Sep 17 00:00:00 2001 From: cjnaz Date: Mon, 30 Mar 2020 23:08:57 -0700 Subject: [PATCH 6/6] v1.0 final --- .vscode/settings.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..e2a4bbd --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "cSpell.words": [ + "digin", + "xbrowsersync", + "xbsjsonedit" + ] +} \ No newline at end of file